diff options
Diffstat (limited to 'webapp/src/app')
138 files changed, 5247 insertions, 2297 deletions
diff --git a/webapp/src/app/@core-xds/core-xds.module.ts b/webapp/src/app/@core-xds/core-xds.module.ts new file mode 100644 index 0000000..c5babc3 --- /dev/null +++ b/webapp/src/app/@core-xds/core-xds.module.ts @@ -0,0 +1,54 @@ +import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { NbAuthModule, NbDummyAuthProvider } from '@nebular/auth'; +import { CookieModule } from 'ngx-cookie'; + +import { throwIfAlreadyLoaded } from './module-import-guard'; +import { XdsServicesModule } from './services/@core-xds-services.module'; +import { AnalyticsService } from '../@core/utils/analytics.service'; +import { StateService } from '../@core/data/state.service'; + +const NB_COREXDS_PROVIDERS = [ + ...XdsServicesModule.forRoot().providers, + ...NbAuthModule.forRoot({ + providers: { + email: { + service: NbDummyAuthProvider, + config: { + delay: 3000, + login: { + rememberMe: true, + }, + }, + }, + }, + }).providers, + AnalyticsService, + StateService, +]; + +@NgModule({ + imports: [ + CommonModule, + CookieModule.forRoot(), + ], + exports: [ + NbAuthModule, + ], + declarations: [], +}) +export class CoreXdsModule { + constructor( @Optional() @SkipSelf() parentModule: CoreXdsModule) { + throwIfAlreadyLoaded(parentModule, 'CoreXdsModule'); + } + + static forRoot(): ModuleWithProviders { + return <ModuleWithProviders>{ + ngModule: CoreXdsModule, + providers: [ + ...NB_COREXDS_PROVIDERS, + ], + }; + } +} diff --git a/webapp/src/app/@core-xds/module-import-guard.ts b/webapp/src/app/@core-xds/module-import-guard.ts new file mode 100644 index 0000000..445640c --- /dev/null +++ b/webapp/src/app/@core-xds/module-import-guard.ts @@ -0,0 +1,5 @@ +export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) { + if (parentModule) { + throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`); + } +} diff --git a/webapp/src/app/@core-xds/services/@core-xds-services.module.ts b/webapp/src/app/@core-xds/services/@core-xds-services.module.ts new file mode 100644 index 0000000..13714e1 --- /dev/null +++ b/webapp/src/app/@core-xds/services/@core-xds-services.module.ts @@ -0,0 +1,39 @@ +import { NgModule, ModuleWithProviders } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { AlertService } from './alert.service'; +import { ConfigService } from './config.service'; +import { ProjectService } from './project.service'; +import { SdkService } from './sdk.service'; +import { UserService } from './users.service'; +import { XDSConfigService } from './xds-config.service'; +import { XDSAgentService } from './xdsagent.service'; + +const SERVICES = [ + AlertService, + ConfigService, + ProjectService, + SdkService, + UserService, + XDSConfigService, + XDSAgentService, +]; + +@NgModule({ + imports: [ + CommonModule, + ], + providers: [ + ...SERVICES, + ], +}) +export class XdsServicesModule { + static forRoot(): ModuleWithProviders { + return <ModuleWithProviders>{ + ngModule: XdsServicesModule, + providers: [ + ...SERVICES, + ], + }; + } +} diff --git a/webapp/src/app/services/alert.service.spec.ts b/webapp/src/app/@core-xds/services/alert.service.spec.ts index b3d364c..b3d364c 100644 --- a/webapp/src/app/services/alert.service.spec.ts +++ b/webapp/src/app/@core-xds/services/alert.service.spec.ts diff --git a/webapp/src/app/@core-xds/services/alert.service.ts b/webapp/src/app/@core-xds/services/alert.service.ts new file mode 100644 index 0000000..c15e176 --- /dev/null +++ b/webapp/src/app/@core-xds/services/alert.service.ts @@ -0,0 +1,73 @@ +import { Injectable, SecurityContext } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + + +export type AlertType = 'error' | 'warning' | 'info' | 'success'; + +export interface IAlert { + type: AlertType; + msg: string; + show?: boolean; + dismissible?: boolean; + dismissTimeout?: number; // close alert after this time (in seconds) + id?: number; +} + +@Injectable() +export class AlertService { + public alerts: Observable<IAlert[]>; + + private _alerts: IAlert[]; + private alertsSubject = <Subject<IAlert[]>>new Subject(); + private uid = 0; + private defaultDismissTmo = 5; // in seconds + + constructor() { + this.alerts = this.alertsSubject.asObservable(); + this._alerts = []; + this.uid = 0; + } + + public error(msg: string, dismissTime?: number) { + this.add({ + type: 'error', msg: msg, dismissible: true, dismissTimeout: dismissTime + }); + } + + public warning(msg: string, dismissible?: boolean) { + this.add({ type: 'warning', msg: msg, dismissible: true, dismissTimeout: (dismissible ? this.defaultDismissTmo : 0) }); + } + + public info(msg: string) { + this.add({ type: 'info', msg: msg, dismissible: true, dismissTimeout: this.defaultDismissTmo }); + } + + public add(al: IAlert) { + const msg = String(al.msg).replace('\n', '<br>'); + // this._alerts.push({ + this._alerts = [{ + show: true, + type: al.type, + msg: msg, + dismissible: al.dismissible || true, + dismissTimeout: (al.dismissTimeout * 1000) || 0, + id: this.uid, + }]; + this.uid += 1; + this.alertsSubject.next(this._alerts); + + } + + public del(al: IAlert) { + /* + const idx = this._alerts.findIndex((a) => a.id === al.id); + if (idx > -1) { + this._alerts.splice(idx, 1); + this.alertsSubject.next(this._alerts); + } + */ + this._alerts = []; + this.alertsSubject.next(this._alerts); + } +} diff --git a/webapp/src/app/services/config.service.spec.ts b/webapp/src/app/@core-xds/services/config.service.spec.ts index a20d4ba..a20d4ba 100644 --- a/webapp/src/app/services/config.service.spec.ts +++ b/webapp/src/app/@core-xds/services/config.service.spec.ts diff --git a/webapp/src/app/services/config.service.ts b/webapp/src/app/@core-xds/services/config.service.ts index ffe2b45..1ba9f2d 100644 --- a/webapp/src/app/services/config.service.ts +++ b/webapp/src/app/@core-xds/services/config.service.ts @@ -4,7 +4,6 @@ import { Observable } from 'rxjs/Observable'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { AlertService, IAlert } from '../services/alert.service'; -import { UtilsService } from '../services/utils.service'; export interface IConfig { language: string; @@ -22,7 +21,6 @@ export class ConfigService { constructor( private cookie: CookieService, private alert: AlertService, - private utils: UtilsService, ) { this.load(); this.confSubject = <BehaviorSubject<IConfig>>new BehaviorSubject(this.confStore); @@ -59,4 +57,5 @@ export class ConfigService { this.confStore.projectsRootDir = p; this.save(); } + } diff --git a/webapp/src/app/services/project.service.spec.ts b/webapp/src/app/@core-xds/services/project.service.spec.ts index b8edfc7..b8edfc7 100644 --- a/webapp/src/app/services/project.service.spec.ts +++ b/webapp/src/app/@core-xds/services/project.service.spec.ts diff --git a/webapp/src/app/@core-xds/services/project.service.ts b/webapp/src/app/@core-xds/services/project.service.ts new file mode 100644 index 0000000..8aeed80 --- /dev/null +++ b/webapp/src/app/@core-xds/services/project.service.ts @@ -0,0 +1,207 @@ +import { Injectable, SecurityContext } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +import { XDSAgentService, IXDSProjectConfig } from '../services/xdsagent.service'; + +/* FIXME: syntax only compatible with TS>2.4.0 +export enum ProjectType { + UNSET = '', + NATIVE_PATHMAP = 'PathMap', + SYNCTHING = 'CloudSync' +} +*/ +export type ProjectTypeEnum = '' | 'PathMap' | 'CloudSync'; +export const ProjectType = { + UNSET: '', + NATIVE_PATHMAP: 'PathMap', + SYNCTHING: 'CloudSync' +}; + +export const ProjectTypes = [ + { value: ProjectType.NATIVE_PATHMAP, display: 'Path mapping' }, + { value: ProjectType.SYNCTHING, display: 'Cloud Sync' } +]; + +export const ProjectStatus = { + ErrorConfig: 'ErrorConfig', + Disable: 'Disable', + Enable: 'Enable', + Pause: 'Pause', + Syncing: 'Syncing' +}; + +export interface IProject { + id?: string; + serverId: string; + label: string; + pathClient: string; + pathServer?: string; + type: ProjectTypeEnum; + status?: string; + isInSync?: boolean; + isUsable?: boolean; + serverPrjDef?: IXDSProjectConfig; + isExpanded?: boolean; + visible?: boolean; + defaultSdkID?: string; +} + +@Injectable() +export class ProjectService { + public Projects$: Observable<IProject[]>; + + private _prjsList: IProject[] = []; + private current: IProject; + private prjsSubject = <BehaviorSubject<IProject[]>>new BehaviorSubject(this._prjsList); + + constructor(private xdsSvr: XDSAgentService) { + this.current = null; + this.Projects$ = this.prjsSubject.asObservable(); + + this.xdsSvr.getProjects().subscribe((projects) => { + this._prjsList = []; + projects.forEach(p => { + this._addProject(p, true); + }); + this.prjsSubject.next(Object.assign([], this._prjsList)); + }); + + // Update Project data + this.xdsSvr.ProjectState$.subscribe(prj => { + const i = this._getProjectIdx(prj.id); + if (i >= 0) { + // XXX for now, only isInSync and status may change + this._prjsList[i].isInSync = prj.isInSync; + this._prjsList[i].status = prj.status; + this._prjsList[i].isUsable = this._isUsableProject(prj); + this.prjsSubject.next(Object.assign([], this._prjsList)); + } + }); + + // Add listener on create and delete project events + this.xdsSvr.addEventListener('event:project-add', (ev) => { + if (ev && ev.data && ev.data.id) { + this._addProject(ev.data); + } else { + console.log('Warning: received events with unknown data: ev=', ev); + } + }); + this.xdsSvr.addEventListener('event:project-delete', (ev) => { + if (ev && ev.data && ev.data.id) { + const idx = this._prjsList.findIndex(item => item.id === ev.data.id); + if (idx === -1) { + console.log('Warning: received events on unknown project id: ev=', ev); + return; + } + this._prjsList.splice(idx, 1); + this.prjsSubject.next(Object.assign([], this._prjsList)); + } else { + console.log('Warning: received events with unknown data: ev=', ev); + } + }); + + } + + public setCurrent(s: IProject) { + this.current = s; + } + + public getCurrent(): IProject { + return this.current; + } + + public getCurrentId(): string { + if (this.current && this.current.id) { + return this.current.id; + } + return ''; + } + + Add(prj: IProject): Observable<IProject> { + const xdsPrj: IXDSProjectConfig = { + id: '', + serverId: prj.serverId, + label: prj.label || '', + clientPath: prj.pathClient.trim(), + serverPath: prj.pathServer, + type: prj.type, + defaultSdkID: prj.defaultSdkID, + }; + // Send config to XDS server + return this.xdsSvr.addProject(xdsPrj) + .map(xp => this._convToIProject(xp)); + } + + Delete(prj: IProject): Observable<IProject> { + const idx = this._getProjectIdx(prj.id); + const delPrj = prj; + if (idx === -1) { + throw new Error('Invalid project id (id=' + prj.id + ')'); + } + return this.xdsSvr.deleteProject(prj.id) + .map(res => delPrj); + } + + Sync(prj: IProject): Observable<string> { + const idx = this._getProjectIdx(prj.id); + if (idx === -1) { + throw new Error('Invalid project id (id=' + prj.id + ')'); + } + return this.xdsSvr.syncProject(prj.id); + } + + private _isUsableProject(p) { + return p && p.isInSync && + (p.status === ProjectStatus.Enable) && + (p.status !== ProjectStatus.Syncing); + } + + private _getProjectIdx(id: string): number { + return this._prjsList.findIndex((item) => item.id === id); + } + + private _convToIProject(rPrj: IXDSProjectConfig): IProject { + // Convert XDSFolderConfig to IProject + const pp: IProject = { + id: rPrj.id, + serverId: rPrj.serverId, + label: rPrj.label, + pathClient: rPrj.clientPath, + pathServer: rPrj.serverPath, + type: rPrj.type, + status: rPrj.status, + isInSync: rPrj.isInSync, + isUsable: this._isUsableProject(rPrj), + defaultSdkID: rPrj.defaultSdkID, + serverPrjDef: Object.assign({}, rPrj), // do a copy + }; + return pp; + } + + private _addProject(rPrj: IXDSProjectConfig, noNext?: boolean): IProject { + + // Convert XDSFolderConfig to IProject + const pp = this._convToIProject(rPrj); + + // add new project + this._prjsList.push(pp); + + // sort project array + this._prjsList.sort((a, b) => { + if (a.label < b.label) { + return -1; + } + if (a.label > b.label) { + return 1; + } + return 0; + }); + + if (!noNext) { + this.prjsSubject.next(Object.assign([], this._prjsList)); + } + + return pp; + } +} diff --git a/webapp/src/app/services/sdk.service.ts b/webapp/src/app/@core-xds/services/sdk.service.ts index d492774..d1daa86 100644 --- a/webapp/src/app/services/sdk.service.ts +++ b/webapp/src/app/@core-xds/services/sdk.service.ts @@ -4,6 +4,8 @@ import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { XDSAgentService } from '../services/xdsagent.service'; +import 'rxjs/add/observable/throw'; + export interface ISdk { id: string; profile: string; @@ -51,4 +53,14 @@ export class SdkService { } return ''; } + + public add(sdk: ISdk): Observable<ISdk> { + // TODO SEB + return Observable.throw('Not implement yet'); + } + + public delete(sdk: ISdk): Observable<ISdk> { + // TODO SEB + return Observable.throw('Not implement yet'); + } } diff --git a/webapp/src/app/@core-xds/services/users.service.ts b/webapp/src/app/@core-xds/services/users.service.ts new file mode 100644 index 0000000..e187c10 --- /dev/null +++ b/webapp/src/app/@core-xds/services/users.service.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; + +let counter = 0; + +@Injectable() +export class UserService { + + private users = { + anonymous: { name: 'Anonymous', picture: 'assets/images/anonymous.png' }, + }; + + private userArray: any[]; + + constructor() { + // this.userArray = Object.values(this.users); + } + + getUsers(): Observable<any> { + return Observable.of(this.users); + } + + getUserArray(): Observable<any[]> { + return Observable.of(this.userArray); + } + + getUser(): Observable<any> { + counter = (counter + 1) % this.userArray.length; + return Observable.of(this.userArray[counter]); + } +} diff --git a/webapp/src/app/@core-xds/services/xds-config.service.ts b/webapp/src/app/@core-xds/services/xds-config.service.ts new file mode 100644 index 0000000..7559673 --- /dev/null +++ b/webapp/src/app/@core-xds/services/xds-config.service.ts @@ -0,0 +1,97 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +import { AlertService, IAlert } from '../services/alert.service'; +import { XDSAgentService, IAgentStatus, IXDServerCfg } from '../../@core-xds/services/xdsagent.service'; + +import 'rxjs/add/operator/publish'; +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/catch'; + + +@Injectable() +export class XDSConfigService { + + // Conf$: Observable<IXdsConfig>; + xdsServers: IXDServerCfg[]; + + // private confSubject: BehaviorSubject<IXdsConfig>; + // private confStore: IXdsConfig; + + private _curServer: IXDServerCfg = { id: '', url: '', connRetry: 0, connected: false }; + private curServer$ = new Subject<IXDServerCfg>(); + + constructor( + private alert: AlertService, + private xdsAgentSvr: XDSAgentService, + ) { + /* + this.confSubject = <BehaviorSubject<IXdsConfig>>new BehaviorSubject(this.confStore); + this.Conf$ = this.confSubject.asObservable(); + */ + + // Update servers list + this.xdsAgentSvr.XdsConfig$.subscribe(cfg => { + if (!cfg || cfg.servers.length < 1) { + return; + } + this.xdsServers = cfg.servers; + this._updateCurServer(); + }); + } + + onCurServer(): Observable<IXDServerCfg> { + return this.curServer$.publish().refCount(); + } + + getCurServer(): IXDServerCfg { + return Object.assign({}, this._curServer); + } + + setCurServer(svr: IXDServerCfg): Observable<IXDServerCfg> { + const curSvr = this._getCurServer(); + + if (!curSvr.connected || curSvr.url !== svr.url) { + return this.xdsAgentSvr.setServerUrl(curSvr.id, svr.url, svr.connRetry) + .map(cfg => this._updateCurServer()) + .catch(err => { + this._curServer.connected = false; + this.curServer$.next(Object.assign({}, this._curServer)); + return Observable.throw(err); + }); + } else { + if (curSvr.connRetry !== svr.connRetry) { + return this.xdsAgentSvr.setServerRetry(curSvr.id, svr.connRetry) + .map(cfg => this._updateCurServer()) + .catch(err => { + this.curServer$.next(Object.assign({}, this._curServer)); + return Observable.throw(err); + }); + } + } + return Observable.of(curSvr); + } + + private _updateCurServer() { + this._curServer = this._getCurServer(); + this.curServer$.next(Object.assign({}, this._curServer)); + } + + private _getCurServer(url?: string): IXDServerCfg { + if (!this.xdsServers) { + return this._curServer; + } + + // Init the 1st time + let svrUrl = url || this._curServer.url; + if (this._curServer.url === '' && this.xdsServers.length > 0) { + svrUrl = this.xdsServers[0].url; + } + + const svr = this.xdsServers.filter(s => s.url === svrUrl); + return svr[0]; + } + +} diff --git a/webapp/src/app/@core-xds/services/xdsagent.service.ts b/webapp/src/app/@core-xds/services/xdsagent.service.ts new file mode 100644 index 0000000..56e493f --- /dev/null +++ b/webapp/src/app/@core-xds/services/xdsagent.service.ts @@ -0,0 +1,397 @@ +import { Injectable, Inject } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import { DOCUMENT } from '@angular/common'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import * as io from 'socket.io-client'; + +import { AlertService } from './alert.service'; +import { ISdk } from './sdk.service'; +import { ProjectType, ProjectTypeEnum } from './project.service'; + +// Import RxJs required methods +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/catch'; +import 'rxjs/add/observable/throw'; +import 'rxjs/add/operator/mergeMap'; +import 'rxjs/add/observable/of'; +import 'rxjs/add/operator/retryWhen'; + + +export interface IXDSConfigProject { + id: string; + path: string; + clientSyncThingID: string; + type: string; + label?: string; + defaultSdkID?: string; +} + +interface IXDSBuilderConfig { + ip: string; + port: string; + syncThingID: string; +} + +export interface IXDSProjectConfig { + id: string; + serverId: string; + label: string; + clientPath: string; + serverPath?: string; + type: ProjectTypeEnum; + status?: string; + isInSync?: boolean; + defaultSdkID: string; +} + +export interface IXDSVer { + id: string; + version: string; + apiVersion: string; + gitTag: string; +} + +export interface IXDSVersions { + client: IXDSVer; + servers: IXDSVer[]; +} + +export interface IXDServerCfg { + id: string; + url: string; + apiUrl?: string; + partialUrl?: string; + connRetry: number; + connected: boolean; +} + +export interface IXDSConfig { + servers: IXDServerCfg[]; +} + +export interface ISdkMessage { + wsID: string; + msgType: string; + data: any; +} + +export interface ICmdOutput { + cmdID: string; + timestamp: string; + stdout: string; + stderr: string; +} + +export interface ICmdExit { + cmdID: string; + timestamp: string; + code: number; + error: string; +} + +export interface IServerStatus { + id: string; + connected: boolean; +} + +export interface IAgentStatus { + connected: boolean; + servers: IServerStatus[]; +} + + +@Injectable() +export class XDSAgentService { + + public XdsConfig$: Observable<IXDSConfig>; + public Status$: Observable<IAgentStatus>; + public ProjectState$ = <Subject<IXDSProjectConfig>>new Subject(); + public CmdOutput$ = <Subject<ICmdOutput>>new Subject(); + public CmdExit$ = <Subject<ICmdExit>>new Subject(); + + private baseUrl: string; + private wsUrl: string; + private _config = <IXDSConfig>{ servers: [] }; + private _status = { connected: false, servers: [] }; + + private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config); + private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status); + + private socket: SocketIOClient.Socket; + + constructor( @Inject(DOCUMENT) private document: Document, + private http: HttpClient, private alert: AlertService) { + + this.XdsConfig$ = this.configSubject.asObservable(); + this.Status$ = this.statusSubject.asObservable(); + + const originUrl = this.document.location.origin; + this.baseUrl = originUrl + '/api/v1'; + + const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/); + if (re === null || re.length < 2) { + console.error('ERROR: cannot determine Websocket url'); + } else { + this.wsUrl = 'ws://' + re[1]; + this._handleIoSocket(); + this._RegisterEvents(); + } + } + + private _NotifyXdsAgentState(sts: boolean) { + this._status.connected = sts; + this.statusSubject.next(Object.assign({}, this._status)); + + // Update XDS config including XDS Server list when connected + if (sts) { + this.getConfig().subscribe(c => { + this._config = c; + this._NotifyXdsServerState(); + this.configSubject.next(Object.assign({ servers: [] }, this._config)); + }); + } + } + + private _NotifyXdsServerState() { + this._status.servers = this._config.servers.map(svr => { + return { id: svr.id, connected: svr.connected }; + }); + this.statusSubject.next(Object.assign({}, this._status)); + } + + private _handleIoSocket() { + this.socket = io(this.wsUrl, { transports: ['websocket'] }); + + this.socket.on('connect_error', (res) => { + this._NotifyXdsAgentState(false); + console.error('XDS Agent WebSocket Connection error !'); + }); + + this.socket.on('connect', (res) => { + this._NotifyXdsAgentState(true); + }); + + this.socket.on('disconnection', (res) => { + this._NotifyXdsAgentState(false); + this.alert.error('WS disconnection: ' + res); + }); + + this.socket.on('error', (err) => { + console.error('WS error:', err); + }); + + this.socket.on('make:output', data => { + this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data)); + }); + + this.socket.on('make:exit', data => { + this.CmdExit$.next(Object.assign({}, <ICmdExit>data)); + }); + + this.socket.on('exec:output', data => { + this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data)); + }); + + this.socket.on('exec:exit', data => { + this.CmdExit$.next(Object.assign({}, <ICmdExit>data)); + }); + + // Events + // (project-add and project-delete events are managed by project.service) + this.socket.on('event:server-config', ev => { + if (ev && ev.data) { + const cfg: IXDServerCfg = ev.data; + const idx = this._config.servers.findIndex(el => el.id === cfg.id); + if (idx >= 0) { + this._config.servers[idx] = Object.assign({}, cfg); + this._NotifyXdsServerState(); + } + this.configSubject.next(Object.assign({}, this._config)); + } + }); + + this.socket.on('event:project-state-change', ev => { + if (ev && ev.data) { + this.ProjectState$.next(Object.assign({}, ev.data)); + } + }); + + } + + /** + ** Events + ***/ + addEventListener(ev: string, fn: Function): SocketIOClient.Emitter { + return this.socket.addEventListener(ev, fn); + } + + /** + ** Misc / Version + ***/ + getVersion(): Observable<IXDSVersions> { + return this._get('/version'); + } + + /*** + ** Config + ***/ + getConfig(): Observable<IXDSConfig> { + return this._get('/config'); + } + + setConfig(cfg: IXDSConfig): Observable<IXDSConfig> { + return this._post('/config', cfg); + } + + setServerRetry(serverID: string, retry: number): Observable<IXDSConfig> { + const svr = this._getServer(serverID); + if (!svr) { + return Observable.throw('Unknown server ID'); + } + if (retry < 0 || Number.isNaN(retry) || retry == null) { + return Observable.throw('Not a valid number'); + } + svr.connRetry = retry; + return this._setConfig(); + } + + setServerUrl(serverID: string, url: string, retry: number): Observable<IXDSConfig> { + const svr = this._getServer(serverID); + if (!svr) { + return Observable.throw('Unknown server ID'); + } + svr.connected = false; + svr.url = url; + if (!Number.isNaN(retry) && retry > 0) { + svr.connRetry = retry; + } + this._NotifyXdsServerState(); + return this._setConfig(); + } + + private _setConfig(): Observable<IXDSConfig> { + return this.setConfig(this._config) + .map(newCfg => { + this._config = newCfg; + this.configSubject.next(Object.assign({}, this._config)); + return this._config; + }); + } + + /*** + ** SDKs + ***/ + getSdks(serverID: string): Observable<ISdk[]> { + const svr = this._getServer(serverID); + if (!svr || !svr.connected) { + return Observable.of([]); + } + + return this._get(svr.partialUrl + '/sdks'); + } + + /*** + ** Projects + ***/ + getProjects(): Observable<IXDSProjectConfig[]> { + return this._get('/projects'); + } + + addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> { + return this._post('/projects', cfg); + } + + deleteProject(id: string): Observable<IXDSProjectConfig> { + return this._delete('/projects/' + id); + } + + syncProject(id: string): Observable<string> { + return this._post('/projects/sync/' + id, {}); + } + + /*** + ** Exec + ***/ + exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> { + return this._post('/exec', + { + id: prjID, + rpath: dir, + cmd: cmd, + sdkID: sdkid || '', + args: args || [], + env: env || [], + }); + } + + /** + ** Private functions + ***/ + + private _RegisterEvents() { + // Register to all existing events + this._post('/events/register', { 'name': 'event:all' }) + .subscribe( + res => { }, + error => { + this.alert.error('ERROR while registering to all events: ' + error); + } + ); + } + + private _getServer(ID: string): IXDServerCfg { + const svr = this._config.servers.filter(item => item.id === ID); + if (svr.length < 1) { + return null; + } + return svr[0]; + } + + private _attachAuthHeaders(options?: any) { + options = options || {}; + const headers = options.headers || new HttpHeaders(); + // headers.append('Authorization', 'Basic ' + btoa('username:password')); + headers.append('Accept', 'application/json'); + headers.append('Content-Type', 'application/json'); + // headers.append('Access-Control-Allow-Origin', '*'); + + options.headers = headers; + return options; + } + + private _get(url: string): Observable<any> { + return this.http.get(this.baseUrl + url, this._attachAuthHeaders()) + .catch(this._decodeError); + } + private _post(url: string, body: any): Observable<any> { + return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders()) + .catch((error) => { + return this._decodeError(error); + }); + } + private _delete(url: string): Observable<any> { + return this.http.delete(this.baseUrl + url, this._attachAuthHeaders()) + .catch(this._decodeError); + } + + private _decodeError(err: any) { + let e: string; + if (err instanceof HttpErrorResponse) { + e = (err.error && err.error.error) ? err.error.error : err.message || 'Unknown error'; + } else if (typeof err === 'object') { + if (err.statusText) { + e = err.statusText; + } else if (err.error) { + e = String(err.error); + } else { + e = JSON.stringify(err); + } + } else { + e = err.message ? err.message : err.toString(); + } + console.log('xdsagent.service - ERROR: ', e); + return Observable.throw(e); + } +} diff --git a/webapp/src/app/@core/data/.gitkeep b/webapp/src/app/@core/data/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/webapp/src/app/@core/data/.gitkeep diff --git a/webapp/src/app/@core/data/state.service.ts b/webapp/src/app/@core/data/state.service.ts new file mode 100644 index 0000000..a6bcb08 --- /dev/null +++ b/webapp/src/app/@core/data/state.service.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import 'rxjs/add/observable/of'; + +@Injectable() +export class StateService { + + protected layouts: any = [ + { + name: 'One Column', + icon: 'nb-layout-default', + id: 'one-column', + selected: true, + }, + { + name: 'Two Column', + icon: 'nb-layout-two-column', + id: 'two-column', + }, + { + name: 'Center Column', + icon: 'nb-layout-centre', + id: 'center-column', + }, + ]; + + protected sidebars: any = [ + { + name: 'Left Sidebar', + icon: 'nb-layout-sidebar-left', + id: 'left', + selected: true, + }, + { + name: 'Right Sidebar', + icon: 'nb-layout-sidebar-right', + id: 'right', + }, + ]; + + protected layoutState$ = new BehaviorSubject(this.layouts[0]); + protected sidebarState$ = new BehaviorSubject(this.sidebars[0]); + + setLayoutState(state: any): any { + this.layoutState$.next(state); + } + + getLayoutStates(): Observable<any[]> { + return Observable.of(this.layouts); + } + + onLayoutState(): Observable<any> { + return this.layoutState$.asObservable(); + } + + setSidebarState(state: any): any { + this.sidebarState$.next(state); + } + + getSidebarStates(): Observable<any[]> { + return Observable.of(this.sidebars); + } + + onSidebarState(): Observable<any> { + return this.sidebarState$.asObservable(); + } +} diff --git a/webapp/src/app/@core/utils/.gitkeep b/webapp/src/app/@core/utils/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/webapp/src/app/@core/utils/.gitkeep diff --git a/webapp/src/app/@core/utils/analytics.service.ts b/webapp/src/app/@core/utils/analytics.service.ts new file mode 100644 index 0000000..73f1332 --- /dev/null +++ b/webapp/src/app/@core/utils/analytics.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { Location } from '@angular/common'; + +import { filter } from 'rxjs/operator/filter'; + +declare const ga: any; + +@Injectable() +export class AnalyticsService { + private enabled: boolean; + + constructor(private location: Location, private router: Router) { + this.enabled = false; + } + + trackPageViews() { + if (this.enabled) { + filter.call(this.router.events, (event) => event instanceof NavigationEnd) + .subscribe(() => { + ga('send', {hitType: 'pageview', page: this.location.path()}); + }); + } + } + + trackEvent(eventName: string) { + if (this.enabled) { + ga('send', 'event', eventName); + } + } +} diff --git a/webapp/src/app/@theme/components/footer/footer.component.scss b/webapp/src/app/@theme/components/footer/footer.component.scss new file mode 100644 index 0000000..78d8114 --- /dev/null +++ b/webapp/src/app/@theme/components/footer/footer.component.scss @@ -0,0 +1,30 @@ +@import '../../styles/themes'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; +@import '~bootstrap/scss/mixins/breakpoints'; + +@include nb-install-component() { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + + .socials { + font-size: 2rem; + + a { + padding: 0.4rem; + color: nb-theme(color-fg); + transition: color ease-out 0.1s; + + &:hover { + color: nb-theme(color-fg-heading); + } + } + } + + @include media-breakpoint-down(is) { + .socials { + font-size: 1.5rem; + } + } +} diff --git a/webapp/src/app/@theme/components/footer/footer.component.ts b/webapp/src/app/@theme/components/footer/footer.component.ts new file mode 100644 index 0000000..8e1e825 --- /dev/null +++ b/webapp/src/app/@theme/components/footer/footer.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngx-footer', + styleUrls: ['./footer.component.scss'], + template: ` + <span class="created-by">Created by + <b><a href="http://iot.bzh" target="_blank">IoT.bzh</a></b> 2017 + + <span style="font-size: small;">(powered by <a href="https://github.com/akveo/ngx-admin" target="_blank">akveo/ngx-admin</a>)</span> + </span> + <!-- MODS_XDS + <div class="socials"> + <a href="#" target="_blank" class="ion ion-social-github"></a> + <a href="#" target="_blank" class="ion ion-social-facebook"></a> + <a href="#" target="_blank" class="ion ion-social-twitter"></a> + <a href="#" target="_blank" class="ion ion-social-linkedin"></a> + </div> + --> + `, +}) +export class FooterComponent { +} diff --git a/webapp/src/app/@theme/components/header/header.component.html b/webapp/src/app/@theme/components/header/header.component.html new file mode 100644 index 0000000..5d5eff6 --- /dev/null +++ b/webapp/src/app/@theme/components/header/header.component.html @@ -0,0 +1,32 @@ +<div class="header-container" + [class.left]="position === 'normal'" + [class.right]="position === 'inverse'"> + <div class="logo-containter"> + <a (click)="toggleSidebar()" href="#" class="navigation"><i class="nb-menu"></i></a> +<!-- MODS_XDS + <div class="logo" (click)="goToHome()">ngx-<span>admin</span></div> +--> + <div class="logo" (click)="goToHome()">XDS <span>dashboard</span></div> + </div> +<!-- MODS_XDS + <ngx-theme-switcher></ngx-theme-switcher> +--> +</div> + +<nb-actions + size="medium" + class="header-container" + [class.right]="position === 'normal'" + [class.left]="position === 'inverse'"> + <nb-action icon="nb-grid-b" class="toggle-layout" (click)="toggleSettings()"></nb-action> + <nb-action> + <nb-user [menu]="userMenu" [name]="user?.name" [picture]="user?.picture"></nb-user> + </nb-action> + <nb-action class="control-item" disabled icon="nb-notifications"></nb-action> +<!-- MODS_XDS + <nb-action class="control-item" icon="nb-email"></nb-action> +--> + <nb-action class="control-item"> + <nb-search type="rotate-layout" (click)="startSearch()"></nb-search> + </nb-action> +</nb-actions> diff --git a/webapp/src/app/@theme/components/header/header.component.scss b/webapp/src/app/@theme/components/header/header.component.scss new file mode 100644 index 0000000..647311b --- /dev/null +++ b/webapp/src/app/@theme/components/header/header.component.scss @@ -0,0 +1,115 @@ +@import '../../styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + display: flex; + justify-content: space-between; + width: 100%; + + .left { + display: flex; + width: 100%; + order: 0; + flex-direction: row; + } + .right { + order: 1; + flex-direction: row-reverse; + } + + .logo-containter { + display: flex; + align-items: center; + } + + .control-item { + display: block; + } + + .header-container { + display: flex; + align-items: center; + width: 100%; + + .navigation { + padding-right: nb-theme(padding); + font-size: 2.5rem; + text-decoration: none; + + i { + display: block; + } + + } + + .logo { + padding: 0 nb-theme(padding); + font-size: 1.75rem; + font-weight: nb-theme(font-weight-bolder); + border-left: 1px solid nb-theme(separator); + white-space: nowrap; + + span { + font-weight: nb-theme(font-weight-normal); + } + } + } + + .toggle-layout /deep/ a { + display: block; + text-decoration: none; + line-height: 1; + + i { + color: nb-theme(color-fg-highlight); + font-size: 2.25rem; + } + } + + @include media-breakpoint-down(md) { + + nb-action:not(.toggle-layout) { + border: none; + } + + .control-item { + display: none; + } + + .toggle-layout { + padding: 0; + } + } + + @include media-breakpoint-down(sm) { + + nb-user /deep/ .user-name { + display: none; + } + } + + @include media-breakpoint-down(is) { + + .header-container { + .logo { + font-size: 1.25rem; + } + } + + .toggle-layout { + display: none; + } + + nb-action:not(.toggle-layout) { + padding: 0; + } + } + + @include media-breakpoint-down(xs) { + .right /deep/ { + display: none; + } + } +} + diff --git a/webapp/src/app/@theme/components/header/header.component.ts b/webapp/src/app/@theme/components/header/header.component.ts new file mode 100644 index 0000000..e2a84cb --- /dev/null +++ b/webapp/src/app/@theme/components/header/header.component.ts @@ -0,0 +1,51 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { NbMenuService, NbSidebarService } from '@nebular/theme'; +// XDS_MODS +import { UserService } from '../../../@core-xds/services/users.service'; +import { AnalyticsService } from '../../../@core/utils/analytics.service'; + +@Component({ + selector: 'ngx-header', + styleUrls: ['./header.component.scss'], + templateUrl: './header.component.html', +}) +export class HeaderComponent implements OnInit { + + + @Input() position = 'normal'; + + user: any; + + userMenu = [{ title: 'Profile' }, { title: 'Log out' }]; + + constructor(private sidebarService: NbSidebarService, + private menuService: NbMenuService, + private userService: UserService, + private analyticsService: AnalyticsService) { + } + + ngOnInit() { + // XDS_MODS + this.userService.getUsers() + .subscribe((users: any) => this.user = users.anonymous); + } + + toggleSidebar(): boolean { + this.sidebarService.toggle(true, 'menu-sidebar'); + return false; + } + + toggleSettings(): boolean { + this.sidebarService.toggle(false, 'settings-sidebar'); + return false; + } + + goToHome() { + this.menuService.navigateHome(); + } + + startSearch() { + this.analyticsService.trackEvent('startSearch'); + } +} diff --git a/webapp/src/app/@theme/components/index.ts b/webapp/src/app/@theme/components/index.ts new file mode 100644 index 0000000..4a25efe --- /dev/null +++ b/webapp/src/app/@theme/components/index.ts @@ -0,0 +1,6 @@ +export * from './header/header.component'; +export * from './footer/footer.component'; +export * from './search-input/search-input.component'; +// XDS_MODS export * from './tiny-mce/tiny-mce.component'; +export * from './theme-settings/theme-settings.component'; +export * from './theme-switcher/theme-switcher.component'; diff --git a/webapp/src/app/@theme/components/search-input/search-input.component.scss b/webapp/src/app/@theme/components/search-input/search-input.component.scss new file mode 100644 index 0000000..5ef07ef --- /dev/null +++ b/webapp/src/app/@theme/components/search-input/search-input.component.scss @@ -0,0 +1,33 @@ +:host { + display: flex; + align-items: center; + + i.control-icon { + &::before { + font-size: 2.3rem; + } + + &:hover { + cursor: pointer; + } + } + + input { + border: none; + outline: none; + margin-left: 1rem; + width: 15rem; + transition: width 0.2s ease; + + &.hidden { + width: 0; + margin: 0; + } + } + + /deep/ search-input { + input { + background: transparent; + } + } +} diff --git a/webapp/src/app/@theme/components/search-input/search-input.component.ts b/webapp/src/app/@theme/components/search-input/search-input.component.ts new file mode 100644 index 0000000..d9f0f10 --- /dev/null +++ b/webapp/src/app/@theme/components/search-input/search-input.component.ts @@ -0,0 +1,35 @@ +import { Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core'; + +@Component({ + selector: 'ngx-search-input', + styleUrls: ['./search-input.component.scss'], + template: ` + <i class="control-icon ion ion-ios-search" + (click)="showInput()"></i> + <input placeholder="Type your search request here..." + #input + [class.hidden]="!isInputShown" + (blur)="hideInput()" + (input)="onInput($event)"> + `, +}) +export class SearchInputComponent { + @ViewChild('input') input: ElementRef; + + @Output() search: EventEmitter<string> = new EventEmitter<string>(); + + isInputShown = false; + + showInput() { + this.isInputShown = true; + this.input.nativeElement.focus(); + } + + hideInput() { + this.isInputShown = false; + } + + onInput(val: string) { + this.search.emit(val); + } +} diff --git a/webapp/src/app/@theme/components/theme-settings/theme-settings.component.scss b/webapp/src/app/@theme/components/theme-settings/theme-settings.component.scss new file mode 100644 index 0000000..4a0a93e --- /dev/null +++ b/webapp/src/app/@theme/components/theme-settings/theme-settings.component.scss @@ -0,0 +1,36 @@ +@import '../../styles/themes'; + +@include nb-install-component() { + h6 { + margin-bottom: 0.5rem; + } + + .settings-row { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + + width: 90%; + margin: 0 0 1rem; + + a { + text-decoration: none; + font-size: 2.25rem; + + color: nb-theme(color-fg); + + &.selected { + color: nb-theme(color-success); + } + + @include nb-for-theme(cosmic) { + &.selected { + color: nb-theme(link-color); + } + } + } + } +} + diff --git a/webapp/src/app/@theme/components/theme-settings/theme-settings.component.ts b/webapp/src/app/@theme/components/theme-settings/theme-settings.component.ts new file mode 100644 index 0000000..9cd60fe --- /dev/null +++ b/webapp/src/app/@theme/components/theme-settings/theme-settings.component.ts @@ -0,0 +1,65 @@ +import { Component, Input, OnInit } from '@angular/core'; + +import { StateService } from '../../../@core/data/state.service'; + +@Component({ + selector: 'ngx-theme-settings', + styleUrls: ['./theme-settings.component.scss'], + template: ` + <h6>LAYOUTS</h6> + <div class="settings-row"> + <a *ngFor="let layout of layouts" + href="#" + [class.selected]="layout.selected" + [attr.title]="layout.name" + (click)="layoutSelect(layout)"> + <i [attr.class]="layout.icon"></i> + </a> + </div> + <h6>SIDEBAR</h6> + <div class="settings-row"> + <a *ngFor="let sidebar of sidebars" + href="#" + [class.selected]="sidebar.selected" + [attr.title]="sidebar.name" + (click)="sidebarSelect(sidebar)"> + <i [attr.class]="sidebar.icon"></i> + </a> + </div> + `, +}) +export class ThemeSettingsComponent { + + layouts = []; + sidebars = []; + + constructor(protected stateService: StateService) { + this.stateService.getLayoutStates() + .subscribe((layouts: any[]) => this.layouts = layouts); + + this.stateService.getSidebarStates() + .subscribe((sidebars: any[]) => this.sidebars = sidebars); + } + + layoutSelect(layout: any): boolean { + this.layouts = this.layouts.map((l: any) => { + l.selected = false; + return l; + }); + + layout.selected = true; + this.stateService.setLayoutState(layout); + return false; + } + + sidebarSelect(sidebars: any): boolean { + this.sidebars = this.sidebars.map((s: any) => { + s.selected = false; + return s; + }); + + sidebars.selected = true; + this.stateService.setSidebarState(sidebars); + return false; + } +} diff --git a/webapp/src/app/@theme/components/theme-switcher/theme-switcher.component.scss b/webapp/src/app/@theme/components/theme-switcher/theme-switcher.component.scss new file mode 100644 index 0000000..210add8 --- /dev/null +++ b/webapp/src/app/@theme/components/theme-switcher/theme-switcher.component.scss @@ -0,0 +1,101 @@ +@import '../../styles/themes'; +@import '~@nebular/theme/styles/global/bootstrap/hero-buttons'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + display: flex; + flex-direction: column; + align-items: center; + width: 50%; + + .theme-switch { + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + margin: 0; + + & > span { + font-size: 1.125rem; + font-weight: nb-theme(font-weight-bold); + transition: opacity 0.3s ease; + + &.light { + color: nb-theme(color-fg-text); + padding-right: 10px; + } + + &.cosmic { + color: nb-theme(color-fg); + padding-left: 10px; + } + + @include nb-for-theme(cosmic) { + &.light { + color: nb-theme(color-fg); + } + + &.cosmic { + color: nb-theme(color-white); + } + } + + &:active { + opacity: 0.78; + } + } + } + + .switch { + position: relative; + display: inline-block; + width: 4rem; + height: 1.75rem; + margin: 0; + + input { + display: none; + + &:checked + .slider::before { + transform: translateX(2.25rem); + } + } + + .slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 1.75rem; + background-color: nb-theme(layout-bg); + } + + .slider::before { + position: absolute; + content: ''; + height: 1.75rem; + width: 1.75rem; + border-radius: 50%; + background-color: nb-theme(color-success); + transition: 0.2s; + + box-shadow: 0 0 0.25rem 0 rgba(nb-theme(color-fg), 0.4); + + @include nb-for-theme(cosmic) { + @include btn-hero-primary-gradient(); + } + } + } + + @include media-breakpoint-down(is) { + .light, .cosmic { + display: none; + } + } + + @include media-breakpoint-down(xs) { + align-items: flex-end; + } +} diff --git a/webapp/src/app/@theme/components/theme-switcher/theme-switcher.component.ts b/webapp/src/app/@theme/components/theme-switcher/theme-switcher.component.ts new file mode 100644 index 0000000..e84b942 --- /dev/null +++ b/webapp/src/app/@theme/components/theme-switcher/theme-switcher.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit } from '@angular/core'; +import { NbThemeService } from '@nebular/theme'; +import { NbJSThemeOptions } from '@nebular/theme/services/js-themes/theme.options'; +import { AnalyticsService } from '../../../@core/utils/analytics.service'; + +@Component({ + selector: 'ngx-theme-switcher', + styleUrls: ['./theme-switcher.component.scss'], + template: ` + <label class="theme-switch"> + <span class="light">Light</span> + <div class="switch"> + <input type="checkbox" [checked]="currentBoolTheme()" (change)="toggleTheme(theme.checked)" #theme> + <span class="slider"></span> + </div> + <span class="cosmic">Cosmic</span> + </label> + `, +}) +export class ThemeSwitcherComponent implements OnInit { + theme: NbJSThemeOptions; + + constructor(private themeService: NbThemeService, private analyticsService: AnalyticsService) { + } + + ngOnInit() { + this.themeService.getJsTheme() + .subscribe((theme: NbJSThemeOptions) => this.theme = theme); + } + + toggleTheme(theme: boolean) { + const boolTheme = this.boolToTheme(theme); + this.themeService.changeTheme(boolTheme); + this.analyticsService.trackEvent('switchTheme'); + } + + currentBoolTheme() { + return this.themeToBool(this.theme); + } + + private themeToBool(theme: NbJSThemeOptions) { + return theme.name === 'cosmic'; + } + + private boolToTheme(theme: boolean) { + return theme ? 'cosmic' : 'default'; + } +} diff --git a/webapp/src/app/@theme/components/tiny-mce/tiny-mce.component.ts b/webapp/src/app/@theme/components/tiny-mce/tiny-mce.component.ts new file mode 100644 index 0000000..c54685b --- /dev/null +++ b/webapp/src/app/@theme/components/tiny-mce/tiny-mce.component.ts @@ -0,0 +1,33 @@ +import { Component, OnDestroy, AfterViewInit, Output, EventEmitter, ElementRef } from '@angular/core'; + +@Component({ + selector: 'ngx-tiny-mce', + template: '', +}) +export class TinyMCEComponent implements OnDestroy, AfterViewInit { + + @Output() editorKeyup = new EventEmitter<any>(); + + editor: any; + + constructor(private host: ElementRef) { } + + ngAfterViewInit() { + tinymce.init({ + target: this.host.nativeElement, + plugins: ['link', 'paste', 'table'], + skin_url: 'assets/skins/lightgray', + setup: editor => { + this.editor = editor; + editor.on('keyup', () => { + this.editorKeyup.emit(editor.getContent()); + }); + }, + height: '320', + }); + } + + ngOnDestroy() { + tinymce.remove(this.editor); + } +} diff --git a/webapp/src/app/@theme/directives/.gitkeep b/webapp/src/app/@theme/directives/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/webapp/src/app/@theme/directives/.gitkeep diff --git a/webapp/src/app/@theme/layouts/index.ts b/webapp/src/app/@theme/layouts/index.ts new file mode 100644 index 0000000..47d2015 --- /dev/null +++ b/webapp/src/app/@theme/layouts/index.ts @@ -0,0 +1,6 @@ +export * from './one-column/one-column.layout'; +export * from './two-columns/two-columns.layout'; +export * from './three-columns/three-columns.layout'; +export * from './sample/sample.layout'; +// XDS_MODS +export * from './xds/xds.layout'; diff --git a/webapp/src/app/@theme/layouts/one-column/one-column.layout.scss b/webapp/src/app/@theme/layouts/one-column/one-column.layout.scss new file mode 100644 index 0000000..7ccf7b7 --- /dev/null +++ b/webapp/src/app/@theme/layouts/one-column/one-column.layout.scss @@ -0,0 +1,130 @@ +@import '../../styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + nb-layout-column.small { + flex: 0.15 !important; + } + + nb-sidebar.settings-sidebar { + $sidebar-width: 7.5rem; + + transition: width 0.3s ease; + width: $sidebar-width; + overflow: hidden; + + &.collapsed { + width: 0; + + /deep/ .main-container { + width: 0; + + .scrollable { + width: $sidebar-width; + padding: 1.25rem; + } + } + } + + /deep/ .main-container { + width: $sidebar-width; + background: nb-theme(color-bg); + transition: width 0.3s ease; + overflow: hidden; + + .scrollable { + width: $sidebar-width; + } + + @include nb-for-theme(cosmic) { + background: nb-theme(layout-bg); + } + } + } + + nb-sidebar.menu-sidebar { + + margin-top: nb-theme(sidebar-header-gap); + + /deep/ .main-container { + height: + calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)} - #{nb-theme(sidebar-header-gap)}) !important; + border-top-right-radius: nb-theme(radius); + } + + /deep/ nb-sidebar-header { + padding-bottom: 0.5rem; + text-align: center; + } + + background: transparent; + + .main-btn { + padding: 0.75rem 2.5rem; + margin-top: -2rem; + font-weight: bold; + transition: padding 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.48); + + i { + font-size: 2rem; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); + } + span { + padding-left: 0.25rem; + } + + i, span { + vertical-align: middle; + } + } + + &.compacted { + + /deep/ nb-sidebar-header { + padding-left: 0; + padding-right: 0; + } + + .main-btn { + width: 46px; + height: 44px; + padding: 0.375rem; + border-radius: 5px; + transition: none; + + span { + display: none; + } + } + } + } + + @include media-breakpoint-down(xs) { + .main-content { + padding: 0.75rem !important; + + } + } + + @include media-breakpoint-down(sm) { + + nb-sidebar.menu-sidebar { + + margin-top: 0; + + /deep/ .main-container { + height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)}) !important; + border-top-right-radius: 0; + + .scrollable { + padding-top: 0; + } + } + } + + .main-btn { + display: none; + } + } +} diff --git a/webapp/src/app/@theme/layouts/one-column/one-column.layout.ts b/webapp/src/app/@theme/layouts/one-column/one-column.layout.ts new file mode 100644 index 0000000..beeaf30 --- /dev/null +++ b/webapp/src/app/@theme/layouts/one-column/one-column.layout.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; + +// TODO: move layouts into the framework +@Component({ + selector: 'ngx-one-column-layout', + styleUrls: ['./one-column.layout.scss'], + template: ` + <nb-layout> + <nb-layout-header fixed> + <ngx-header></ngx-header> + </nb-layout-header> + + <nb-sidebar class="menu-sidebar" tag="menu-sidebar" responsive> + <nb-sidebar-header> + <!-- XDS_MODS + <a href="#" class="btn btn-hero-success main-btn"> + <i class="ion ion-social-github"></i> <span>Support Us</span> + </a> + --> + </nb-sidebar-header> + <ng-content select="nb-menu"></ng-content> + </nb-sidebar> + + <nb-layout-column> + <ng-content select="router-outlet"></ng-content> + </nb-layout-column> + + <nb-layout-footer fixed> + <ngx-footer></ngx-footer> + </nb-layout-footer> + </nb-layout> + `, +}) +export class OneColumnLayoutComponent { +} diff --git a/webapp/src/app/@theme/layouts/sample/sample.layout.scss b/webapp/src/app/@theme/layouts/sample/sample.layout.scss new file mode 100644 index 0000000..7ccf7b7 --- /dev/null +++ b/webapp/src/app/@theme/layouts/sample/sample.layout.scss @@ -0,0 +1,130 @@ +@import '../../styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + nb-layout-column.small { + flex: 0.15 !important; + } + + nb-sidebar.settings-sidebar { + $sidebar-width: 7.5rem; + + transition: width 0.3s ease; + width: $sidebar-width; + overflow: hidden; + + &.collapsed { + width: 0; + + /deep/ .main-container { + width: 0; + + .scrollable { + width: $sidebar-width; + padding: 1.25rem; + } + } + } + + /deep/ .main-container { + width: $sidebar-width; + background: nb-theme(color-bg); + transition: width 0.3s ease; + overflow: hidden; + + .scrollable { + width: $sidebar-width; + } + + @include nb-for-theme(cosmic) { + background: nb-theme(layout-bg); + } + } + } + + nb-sidebar.menu-sidebar { + + margin-top: nb-theme(sidebar-header-gap); + + /deep/ .main-container { + height: + calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)} - #{nb-theme(sidebar-header-gap)}) !important; + border-top-right-radius: nb-theme(radius); + } + + /deep/ nb-sidebar-header { + padding-bottom: 0.5rem; + text-align: center; + } + + background: transparent; + + .main-btn { + padding: 0.75rem 2.5rem; + margin-top: -2rem; + font-weight: bold; + transition: padding 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.48); + + i { + font-size: 2rem; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); + } + span { + padding-left: 0.25rem; + } + + i, span { + vertical-align: middle; + } + } + + &.compacted { + + /deep/ nb-sidebar-header { + padding-left: 0; + padding-right: 0; + } + + .main-btn { + width: 46px; + height: 44px; + padding: 0.375rem; + border-radius: 5px; + transition: none; + + span { + display: none; + } + } + } + } + + @include media-breakpoint-down(xs) { + .main-content { + padding: 0.75rem !important; + + } + } + + @include media-breakpoint-down(sm) { + + nb-sidebar.menu-sidebar { + + margin-top: 0; + + /deep/ .main-container { + height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)}) !important; + border-top-right-radius: 0; + + .scrollable { + padding-top: 0; + } + } + } + + .main-btn { + display: none; + } + } +} diff --git a/webapp/src/app/@theme/layouts/sample/sample.layout.ts b/webapp/src/app/@theme/layouts/sample/sample.layout.ts new file mode 100644 index 0000000..6d35c09 --- /dev/null +++ b/webapp/src/app/@theme/layouts/sample/sample.layout.ts @@ -0,0 +1,145 @@ +import { Component, OnDestroy } from '@angular/core'; +import { + NbMediaBreakpoint, + NbMediaBreakpointsService, + NbMenuItem, + NbMenuService, + NbSidebarService, + NbThemeService, +} from '@nebular/theme'; + +import { StateService } from '../../../@core/data/state.service'; + +import { Subscription } from 'rxjs/Subscription'; +import 'rxjs/add/operator/withLatestFrom'; +import 'rxjs/add/operator/delay'; + +// TODO: move layouts into the framework +@Component({ + selector: 'ngx-sample-layout', + styleUrls: ['./sample.layout.scss'], + template: ` + <nb-layout [center]="layout.id === 'center-column'" windowMode> + <nb-layout-header fixed> + <ngx-header [position]="sidebar.id === 'left' ? 'normal': 'inverse'"></ngx-header> + </nb-layout-header> + + <nb-sidebar class="menu-sidebar" + tag="menu-sidebar" + responsive + [right]="sidebar.id === 'right'"> + <nb-sidebar-header> + <a href="#" class="btn btn-hero-success main-btn"> + <i class="ion ion-social-github"></i> <span>Support Us</span> + </a> + </nb-sidebar-header> + <ng-content select="nb-menu"></ng-content> + </nb-sidebar> + + <nb-layout-column class="main-content"> + <ng-content select="router-outlet"></ng-content> + </nb-layout-column> + + <nb-layout-column left class="small" *ngIf="layout.id === 'two-column' || layout.id === 'three-column'"> + <nb-menu [items]="subMenu"></nb-menu> + </nb-layout-column> + + <nb-layout-column right class="small" *ngIf="layout.id === 'three-column'"> + <nb-menu [items]="subMenu"></nb-menu> + </nb-layout-column> + + <nb-layout-footer fixed> + <ngx-footer></ngx-footer> + </nb-layout-footer> + + <nb-sidebar class="settings-sidebar" + tag="settings-sidebar" + state="collapsed" + fixed + [right]="sidebar.id !== 'right'"> + <ngx-theme-settings></ngx-theme-settings> + </nb-sidebar> + </nb-layout> + `, +}) +export class SampleLayoutComponent implements OnDestroy { + + subMenu: NbMenuItem[] = [ + { + title: 'PAGE LEVEL MENU', + group: true, + }, + { + title: 'Buttons', + icon: 'ion ion-android-radio-button-off', + link: '/pages/ui-features/buttons', + }, + { + title: 'Grid', + icon: 'ion ion-android-radio-button-off', + link: '/pages/ui-features/grid', + }, + { + title: 'Icons', + icon: 'ion ion-android-radio-button-off', + link: '/pages/ui-features/icons', + }, + { + title: 'Modals', + icon: 'ion ion-android-radio-button-off', + link: '/pages/ui-features/modals', + }, + { + title: 'Typography', + icon: 'ion ion-android-radio-button-off', + link: '/pages/ui-features/typography', + }, + { + title: 'Animated Searches', + icon: 'ion ion-android-radio-button-off', + link: '/pages/ui-features/search-fields', + }, + { + title: 'Tabs', + icon: 'ion ion-android-radio-button-off', + link: '/pages/ui-features/tabs', + }, + ]; + layout: any = {}; + sidebar: any = {}; + + protected layoutState$: Subscription; + protected sidebarState$: Subscription; + protected menuClick$: Subscription; + + constructor(protected stateService: StateService, + protected menuService: NbMenuService, + protected themeService: NbThemeService, + protected bpService: NbMediaBreakpointsService, + protected sidebarService: NbSidebarService) { + this.layoutState$ = this.stateService.onLayoutState() + .subscribe((layout: string) => this.layout = layout); + + this.sidebarState$ = this.stateService.onSidebarState() + .subscribe((sidebar: string) => { + this.sidebar = sidebar; + }); + + const isBp = this.bpService.getByName('is'); + this.menuClick$ = this.menuService.onItemSelect() + .withLatestFrom(this.themeService.onMediaQueryChange()) + .delay(20) + .subscribe(([item, [bpFrom, bpTo]]: [any, [NbMediaBreakpoint, NbMediaBreakpoint]]) => { + + if (bpTo.width <= isBp.width) { + this.sidebarService.collapse('menu-sidebar'); + } + }); + } + + ngOnDestroy() { + this.layoutState$.unsubscribe(); + this.sidebarState$.unsubscribe(); + this.menuClick$.unsubscribe(); + } +} diff --git a/webapp/src/app/@theme/layouts/three-columns/three-columns.layout.scss b/webapp/src/app/@theme/layouts/three-columns/three-columns.layout.scss new file mode 100644 index 0000000..7ccf7b7 --- /dev/null +++ b/webapp/src/app/@theme/layouts/three-columns/three-columns.layout.scss @@ -0,0 +1,130 @@ +@import '../../styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + nb-layout-column.small { + flex: 0.15 !important; + } + + nb-sidebar.settings-sidebar { + $sidebar-width: 7.5rem; + + transition: width 0.3s ease; + width: $sidebar-width; + overflow: hidden; + + &.collapsed { + width: 0; + + /deep/ .main-container { + width: 0; + + .scrollable { + width: $sidebar-width; + padding: 1.25rem; + } + } + } + + /deep/ .main-container { + width: $sidebar-width; + background: nb-theme(color-bg); + transition: width 0.3s ease; + overflow: hidden; + + .scrollable { + width: $sidebar-width; + } + + @include nb-for-theme(cosmic) { + background: nb-theme(layout-bg); + } + } + } + + nb-sidebar.menu-sidebar { + + margin-top: nb-theme(sidebar-header-gap); + + /deep/ .main-container { + height: + calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)} - #{nb-theme(sidebar-header-gap)}) !important; + border-top-right-radius: nb-theme(radius); + } + + /deep/ nb-sidebar-header { + padding-bottom: 0.5rem; + text-align: center; + } + + background: transparent; + + .main-btn { + padding: 0.75rem 2.5rem; + margin-top: -2rem; + font-weight: bold; + transition: padding 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.48); + + i { + font-size: 2rem; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); + } + span { + padding-left: 0.25rem; + } + + i, span { + vertical-align: middle; + } + } + + &.compacted { + + /deep/ nb-sidebar-header { + padding-left: 0; + padding-right: 0; + } + + .main-btn { + width: 46px; + height: 44px; + padding: 0.375rem; + border-radius: 5px; + transition: none; + + span { + display: none; + } + } + } + } + + @include media-breakpoint-down(xs) { + .main-content { + padding: 0.75rem !important; + + } + } + + @include media-breakpoint-down(sm) { + + nb-sidebar.menu-sidebar { + + margin-top: 0; + + /deep/ .main-container { + height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)}) !important; + border-top-right-radius: 0; + + .scrollable { + padding-top: 0; + } + } + } + + .main-btn { + display: none; + } + } +} diff --git a/webapp/src/app/@theme/layouts/three-columns/three-columns.layout.ts b/webapp/src/app/@theme/layouts/three-columns/three-columns.layout.ts new file mode 100644 index 0000000..447b23e --- /dev/null +++ b/webapp/src/app/@theme/layouts/three-columns/three-columns.layout.ts @@ -0,0 +1,39 @@ +import { Component } from '@angular/core'; + +// TODO: move layouts into the framework +@Component({ + selector: 'ngx-three-columns-layout', + styleUrls: ['./three-columns.layout.scss'], + template: ` + <nb-layout> + <nb-layout-header fixed> + <ngx-header></ngx-header> + </nb-layout-header> + + <nb-sidebar class="menu-sidebar" tag="menu-sidebar" responsive > + <nb-sidebar-header> + <a href="#" class="btn btn-hero-success main-btn"> + <i class="ion ion-social-github"></i> <span>Support Us</span> + </a> + </nb-sidebar-header> + <ng-content select="nb-menu"></ng-content> + </nb-sidebar> + + <nb-layout-column class="small"> + </nb-layout-column> + + <nb-layout-column right> + <ng-content select="router-outlet"></ng-content> + </nb-layout-column> + + <nb-layout-column class="small"> + </nb-layout-column> + + <nb-layout-footer fixed> + <ngx-footer></ngx-footer> + </nb-layout-footer> + </nb-layout> + `, +}) +export class ThreeColumnsLayoutComponent { +} diff --git a/webapp/src/app/@theme/layouts/two-columns/two-columns.layout.scss b/webapp/src/app/@theme/layouts/two-columns/two-columns.layout.scss new file mode 100644 index 0000000..7ccf7b7 --- /dev/null +++ b/webapp/src/app/@theme/layouts/two-columns/two-columns.layout.scss @@ -0,0 +1,130 @@ +@import '../../styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + nb-layout-column.small { + flex: 0.15 !important; + } + + nb-sidebar.settings-sidebar { + $sidebar-width: 7.5rem; + + transition: width 0.3s ease; + width: $sidebar-width; + overflow: hidden; + + &.collapsed { + width: 0; + + /deep/ .main-container { + width: 0; + + .scrollable { + width: $sidebar-width; + padding: 1.25rem; + } + } + } + + /deep/ .main-container { + width: $sidebar-width; + background: nb-theme(color-bg); + transition: width 0.3s ease; + overflow: hidden; + + .scrollable { + width: $sidebar-width; + } + + @include nb-for-theme(cosmic) { + background: nb-theme(layout-bg); + } + } + } + + nb-sidebar.menu-sidebar { + + margin-top: nb-theme(sidebar-header-gap); + + /deep/ .main-container { + height: + calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)} - #{nb-theme(sidebar-header-gap)}) !important; + border-top-right-radius: nb-theme(radius); + } + + /deep/ nb-sidebar-header { + padding-bottom: 0.5rem; + text-align: center; + } + + background: transparent; + + .main-btn { + padding: 0.75rem 2.5rem; + margin-top: -2rem; + font-weight: bold; + transition: padding 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.48); + + i { + font-size: 2rem; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); + } + span { + padding-left: 0.25rem; + } + + i, span { + vertical-align: middle; + } + } + + &.compacted { + + /deep/ nb-sidebar-header { + padding-left: 0; + padding-right: 0; + } + + .main-btn { + width: 46px; + height: 44px; + padding: 0.375rem; + border-radius: 5px; + transition: none; + + span { + display: none; + } + } + } + } + + @include media-breakpoint-down(xs) { + .main-content { + padding: 0.75rem !important; + + } + } + + @include media-breakpoint-down(sm) { + + nb-sidebar.menu-sidebar { + + margin-top: 0; + + /deep/ .main-container { + height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)}) !important; + border-top-right-radius: 0; + + .scrollable { + padding-top: 0; + } + } + } + + .main-btn { + display: none; + } + } +} diff --git a/webapp/src/app/@theme/layouts/two-columns/two-columns.layout.ts b/webapp/src/app/@theme/layouts/two-columns/two-columns.layout.ts new file mode 100644 index 0000000..b7f3a67 --- /dev/null +++ b/webapp/src/app/@theme/layouts/two-columns/two-columns.layout.ts @@ -0,0 +1,37 @@ +import { Component } from '@angular/core'; + +// TODO: move layouts into the framework +@Component({ + selector: 'ngx-two-columns-layout', + styleUrls: ['./two-columns.layout.scss'], + template: ` + <nb-layout> + <nb-layout-header fixed> + <ngx-header></ngx-header> + </nb-layout-header> + + <nb-sidebar class="menu-sidebar" tag="menu-sidebar" responsive > + <nb-sidebar-header> + <a href="#" class="btn btn-hero-success main-btn"> + <i class="ion ion-social-github"></i> <span>Support Us</span> + </a> + </nb-sidebar-header> + <ng-content select="nb-menu"></ng-content> + </nb-sidebar> + + <nb-layout-column class="small"> + </nb-layout-column> + + <nb-layout-column right> + <ng-content select="router-outlet"></ng-content> + </nb-layout-column> + + <nb-layout-footer fixed> + <ngx-footer></ngx-footer> + </nb-layout-footer> + + </nb-layout> + `, +}) +export class TwoColumnsLayoutComponent { +} diff --git a/webapp/src/app/@theme/layouts/xds/xds.layout.html b/webapp/src/app/@theme/layouts/xds/xds.layout.html new file mode 100644 index 0000000..bf2b4e6 --- /dev/null +++ b/webapp/src/app/@theme/layouts/xds/xds.layout.html @@ -0,0 +1,46 @@ +<nb-layout [center]="layout.id === 'center-column'" windowMode> + + <nb-layout-header fixed> + <ngx-header [position]="sidebar.id === 'left' ? 'normal': 'inverse'"></ngx-header> + </nb-layout-header> + + <nb-sidebar class="menu-sidebar" tag="menu-sidebar" responsive [right]="sidebar.id === 'right'"> + + <nb-sidebar-header (click)="toogleSidebar()"> + <!-- XXX - ugly rework --> + <nb-actions *ngIf="sidebar.id === 'left'" size="small" class="header-container right"> + <nb-action *ngIf="!sidebarCompact" icon="fa fa-angle-double-left" style="margin-left: 80%;"></nb-action> + <nb-action *ngIf="sidebarCompact" icon="fa fa-angle-double-right"></nb-action> + </nb-actions> + <nb-actions *ngIf="sidebar.id === 'right'" size="small" class="header-container left"> + <nb-action *ngIf="!sidebarCompact" icon="fa fa-angle-double-right" style="margin-right: 80%;"></nb-action> + <nb-action *ngIf="sidebarCompact" icon="fa fa-angle-double-left"></nb-action> + </nb-actions> + </nb-sidebar-header> + + <ng-content select="nb-menu"></ng-content> + + <nb-sidebar-footer> + </nb-sidebar-footer> + </nb-sidebar> + + <nb-layout-column class="main-content"> + <ng-content select="router-outlet"></ng-content> + </nb-layout-column> + + <nb-layout-column left class="small" *ngIf="layout.id === 'two-column' || layout.id === 'three-column'"> + <nb-menu [items]="subMenu"></nb-menu> + </nb-layout-column> + + <nb-layout-column right class="small" *ngIf="layout.id === 'three-column'"> + <nb-menu [items]="subMenu"></nb-menu> + </nb-layout-column> + + <nb-layout-footer fixed> + <ngx-footer></ngx-footer> + </nb-layout-footer> + + <nb-sidebar class="settings-sidebar" tag="settings-sidebar" state="collapsed" fixed [right]="sidebar.id !== 'right'"> + <ngx-theme-settings></ngx-theme-settings> + </nb-sidebar> +</nb-layout> diff --git a/webapp/src/app/@theme/layouts/xds/xds.layout.scss b/webapp/src/app/@theme/layouts/xds/xds.layout.scss new file mode 100644 index 0000000..7ccf7b7 --- /dev/null +++ b/webapp/src/app/@theme/layouts/xds/xds.layout.scss @@ -0,0 +1,130 @@ +@import '../../styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + nb-layout-column.small { + flex: 0.15 !important; + } + + nb-sidebar.settings-sidebar { + $sidebar-width: 7.5rem; + + transition: width 0.3s ease; + width: $sidebar-width; + overflow: hidden; + + &.collapsed { + width: 0; + + /deep/ .main-container { + width: 0; + + .scrollable { + width: $sidebar-width; + padding: 1.25rem; + } + } + } + + /deep/ .main-container { + width: $sidebar-width; + background: nb-theme(color-bg); + transition: width 0.3s ease; + overflow: hidden; + + .scrollable { + width: $sidebar-width; + } + + @include nb-for-theme(cosmic) { + background: nb-theme(layout-bg); + } + } + } + + nb-sidebar.menu-sidebar { + + margin-top: nb-theme(sidebar-header-gap); + + /deep/ .main-container { + height: + calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)} - #{nb-theme(sidebar-header-gap)}) !important; + border-top-right-radius: nb-theme(radius); + } + + /deep/ nb-sidebar-header { + padding-bottom: 0.5rem; + text-align: center; + } + + background: transparent; + + .main-btn { + padding: 0.75rem 2.5rem; + margin-top: -2rem; + font-weight: bold; + transition: padding 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.48); + + i { + font-size: 2rem; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); + } + span { + padding-left: 0.25rem; + } + + i, span { + vertical-align: middle; + } + } + + &.compacted { + + /deep/ nb-sidebar-header { + padding-left: 0; + padding-right: 0; + } + + .main-btn { + width: 46px; + height: 44px; + padding: 0.375rem; + border-radius: 5px; + transition: none; + + span { + display: none; + } + } + } + } + + @include media-breakpoint-down(xs) { + .main-content { + padding: 0.75rem !important; + + } + } + + @include media-breakpoint-down(sm) { + + nb-sidebar.menu-sidebar { + + margin-top: 0; + + /deep/ .main-container { + height: calc(#{nb-theme(sidebar-height)} - #{nb-theme(header-height)}) !important; + border-top-right-radius: 0; + + .scrollable { + padding-top: 0; + } + } + } + + .main-btn { + display: none; + } + } +} diff --git a/webapp/src/app/@theme/layouts/xds/xds.layout.ts b/webapp/src/app/@theme/layouts/xds/xds.layout.ts new file mode 100644 index 0000000..8987584 --- /dev/null +++ b/webapp/src/app/@theme/layouts/xds/xds.layout.ts @@ -0,0 +1,76 @@ +import { Component, OnDestroy } from '@angular/core'; +import { + NbMediaBreakpoint, + NbMediaBreakpointsService, + NbMenuItem, + NbMenuService, + NbSidebarService, + NbThemeService, +} from '@nebular/theme'; + +import { StateService } from '../../../@core/data/state.service'; + +import { Subscription } from 'rxjs/Subscription'; +import 'rxjs/add/operator/withLatestFrom'; +import 'rxjs/add/operator/delay'; + +// TODO: move layouts into the framework +@Component({ + selector: 'ngx-xds-layout', + styleUrls: ['./xds.layout.scss'], + templateUrl: './xds.layout.html', +}) + +export class XdsLayoutComponent implements OnDestroy { + + subMenu: NbMenuItem[] = []; + layout: any = {}; + sidebar: any = {}; + sidebarCompact = true; + + protected layoutState$: Subscription; + protected sidebarState$: Subscription; + protected menuClick$: Subscription; + + constructor(protected stateService: StateService, + protected menuService: NbMenuService, + protected themeService: NbThemeService, + protected bpService: NbMediaBreakpointsService, + protected sidebarService: NbSidebarService) { + this.layoutState$ = this.stateService.onLayoutState() + .subscribe((layout: string) => this.layout = layout); + + this.sidebarState$ = this.stateService.onSidebarState() + .subscribe((sidebar: string) => { + this.sidebar = sidebar; + }); + + const isBp = this.bpService.getByName('is'); + this.menuClick$ = this.menuService.onItemSelect() + .withLatestFrom(this.themeService.onMediaQueryChange()) + .delay(20) + .subscribe(([item, [bpFrom, bpTo]]: [any, [NbMediaBreakpoint, NbMediaBreakpoint]]) => { + + this.sidebarCompact = false; + if (bpTo.width <= isBp.width) { + this.sidebarService.collapse('menu-sidebar'); + } + }); + + // Set sidebarCompact according to sidebar state changes + this.sidebarService.onToggle().subscribe(s => s.tag === 'menu-sidebar' && (this.sidebarCompact = !this.sidebarCompact)); + this.sidebarService.onCollapse().subscribe(s => s.tag === 'menu-sidebar' && (this.sidebarCompact = true)); + this.sidebarService.onExpand().subscribe(() => this.sidebarCompact = false); + this.menuService.onSubmenuToggle().subscribe(i => i.item && i.item.expanded && (this.sidebarCompact = false)); + } + + toogleSidebar() { + this.sidebarService.toggle(true, 'menu-sidebar'); + } + + ngOnDestroy() { + this.layoutState$.unsubscribe(); + this.sidebarState$.unsubscribe(); + this.menuClick$.unsubscribe(); + } +} diff --git a/webapp/src/app/@theme/pipes/.gitkeep b/webapp/src/app/@theme/pipes/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/webapp/src/app/@theme/pipes/.gitkeep diff --git a/webapp/src/app/@theme/pipes/capitalize.pipe.ts b/webapp/src/app/@theme/pipes/capitalize.pipe.ts new file mode 100644 index 0000000..61d5e58 --- /dev/null +++ b/webapp/src/app/@theme/pipes/capitalize.pipe.ts @@ -0,0 +1,11 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'ngxCapitalize' }) +export class CapitalizePipe implements PipeTransform { + + transform(input: string): string { + return input && input.length + ? (input.charAt(0).toUpperCase() + input.slice(1).toLowerCase()) + : input; + } +} diff --git a/webapp/src/app/@theme/pipes/index.ts b/webapp/src/app/@theme/pipes/index.ts new file mode 100644 index 0000000..541ebeb --- /dev/null +++ b/webapp/src/app/@theme/pipes/index.ts @@ -0,0 +1,4 @@ +export * from './capitalize.pipe'; +export * from './plural.pipe'; +export * from './round.pipe'; +export * from './timing.pipe'; diff --git a/webapp/src/app/@theme/pipes/plural.pipe.ts b/webapp/src/app/@theme/pipes/plural.pipe.ts new file mode 100644 index 0000000..4c34096 --- /dev/null +++ b/webapp/src/app/@theme/pipes/plural.pipe.ts @@ -0,0 +1,14 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'ngxPlural' }) +export class PluralPipe implements PipeTransform { + + transform(input: number, label: string, pluralLabel: string = ''): string { + input = input || 0; + return input === 1 + ? `${input} ${label}` + : pluralLabel + ? `${input} ${pluralLabel}` + : `${input} ${label}s`; + } +} diff --git a/webapp/src/app/@theme/pipes/round.pipe.ts b/webapp/src/app/@theme/pipes/round.pipe.ts new file mode 100644 index 0000000..3ec880f --- /dev/null +++ b/webapp/src/app/@theme/pipes/round.pipe.ts @@ -0,0 +1,9 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'ngxRound' }) +export class RoundPipe implements PipeTransform { + + transform(input: number): number { + return Math.round(input); + } +} diff --git a/webapp/src/app/@theme/pipes/timing.pipe.ts b/webapp/src/app/@theme/pipes/timing.pipe.ts new file mode 100644 index 0000000..afc9056 --- /dev/null +++ b/webapp/src/app/@theme/pipes/timing.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ name: 'timing' }) +export class TimingPipe implements PipeTransform { + transform(time: number): string { + if (time) { + const minutes = Math.floor(time / 60); + const seconds = Math.floor(time % 60); + return `${this.initZero(minutes)}${minutes}:${this.initZero(seconds)}${seconds}`; + } + + return '00:00'; + } + + private initZero(time: number): string { + return time < 10 ? '0' : ''; + } +} diff --git a/webapp/src/app/@theme/styles/pace.theme.scss b/webapp/src/app/@theme/styles/pace.theme.scss new file mode 100644 index 0000000..e3bc9b6 --- /dev/null +++ b/webapp/src/app/@theme/styles/pace.theme.scss @@ -0,0 +1,22 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + + @mixin ngx-pace-theme() { + + .pace .pace-progress { + background: nb-theme(color-fg-highlight); + } + + .pace .pace-progress-inner { + box-shadow: 0 0 10px nb-theme(color-fg-highlight), 0 0 5px nb-theme(color-fg-highlight); + } + + .pace .pace-activity { + display: none; + // border-top-color: nb-theme(color-fg-highlight); + // border-left-color: nb-theme(color-fg-highlight); + } +} diff --git a/webapp/src/app/@theme/styles/styles.scss b/webapp/src/app/@theme/styles/styles.scss new file mode 100644 index 0000000..a943e7a --- /dev/null +++ b/webapp/src/app/@theme/styles/styles.scss @@ -0,0 +1,20 @@ +// themes - our custom or/and out of the box themes +@import 'themes'; + +// framework component themes (styles tied to theme variables) +@import '~@nebular/theme/styles/globals'; +@import '~@nebular/auth/styles/all'; + +// loading progress bar theme +@import './pace.theme'; + +// install the framework and custom global styles +@include nb-install() { + + // framework global styles + @include nb-theme-global(); + @include nb-auth-global(); + + // loading progress bar + @include ngx-pace-theme(); +}; diff --git a/webapp/src/app/@theme/styles/theme.cosmic.ts b/webapp/src/app/@theme/styles/theme.cosmic.ts new file mode 100644 index 0000000..a252beb --- /dev/null +++ b/webapp/src/app/@theme/styles/theme.cosmic.ts @@ -0,0 +1,87 @@ +export const COSMIC_THEME = { + name: 'cosmic', + base: 'default', + variables: { + + temperature: [ + '#2ec7fe', + '#31ffad', + '#7bff24', + '#fff024', + '#f7bd59', + ], + + solar: { + gradientLeft: '#7bff24', + gradientRight: '#2ec7fe', + shadowColor: '#19977E', + radius: ['70%', '90%'], + }, + + traffic: { + colorBlack: '#000000', + tooltipBg: 'rgba(0, 255, 170, 0.35)', + tooltipBorderColor: '#00d977', + tooltipExtraCss: 'box-shadow: 0px 2px 46px 0 rgba(0, 255, 170, 0.35); border-radius: 10px; padding: 4px 16px;', + tooltipTextColor: '#ffffff', + tooltipFontWeight: 'normal', + + lineBg: '#d1d1ff', + lineShadowBlur: '14', + itemColor: '#BEBBFF', + itemBorderColor: '#ffffff', + itemEmphasisBorderColor: '#ffffff', + shadowLineDarkBg: '#655ABD', + shadowLineShadow: 'rgba(33, 7, 77, 0.5)', + gradFrom: 'rgba(118, 89, 255, 0.4)', + gradTo: 'rgba(164, 84, 255, 0.5)', + }, + + electricity: { + tooltipBg: 'rgba(0, 255, 170, 0.35)', + tooltipLineColor: 'rgba(255, 255, 255, 0.1)', + tooltipLineWidth: '1', + tooltipBorderColor: '#00d977', + tooltipExtraCss: 'box-shadow: 0px 2px 46px 0 rgba(0, 255, 170, 0.35); border-radius: 10px; padding: 8px 24px;', + tooltipTextColor: '#ffffff', + tooltipFontWeight: 'normal', + + axisLineColor: 'rgba(161, 161 ,229, 0.3)', + xAxisTextColor: '#a1a1e5', + yAxisSplitLine: 'rgba(161, 161 ,229, 0.2)', + + itemBorderColor: '#ffffff', + lineStyle: 'dotted', + lineWidth: '6', + lineGradFrom: '#00ffaa', + lineGradTo: '#fff835', + lineShadow: 'rgba(14, 16, 48, 0.4)', + + areaGradFrom: 'rgba(188, 92, 255, 0.5)', + areaGradTo: 'rgba(188, 92, 255, 0)', + shadowLineDarkBg: '#a695ff', + }, + + bubbleMap: { + titleColor: '#ffffff', + areaColor: '#2c2961', + areaHoverColor: '#a1a1e5', + areaBorderColor: '#654ddb', + }, + + echarts: { + bg: '#3d3780', + textColor: '#ffffff', + axisLineColor: '#a1a1e5', + splitLineColor: '#342e73', + itemHoverShadowColor: 'rgba(0, 0, 0, 0.5)', + tooltipBackgroundColor: '#6a7985', + areaOpacity: '1', + }, + + chartjs: { + axisLineColor: '#a1a1e5', + textColor: '#ffffff', + }, + }, +}; diff --git a/webapp/src/app/@theme/styles/theme.default.ts b/webapp/src/app/@theme/styles/theme.default.ts new file mode 100644 index 0000000..d387c57 --- /dev/null +++ b/webapp/src/app/@theme/styles/theme.default.ts @@ -0,0 +1,88 @@ +export const DEFAULT_THEME = { + name: 'default', + base: null, + variables: { + + // Safari fix + temperature: [ + '#42db7d', + '#42db7d', + '#42db7d', + '#42db7d', + '#42db7d', + ], + + solar: { + gradientLeft: '#42db7d', + gradientRight: '#42db7d', + shadowColor: 'rgba(0, 0, 0, 0)', + radius: ['80%', '90%'], + }, + + traffic: { + colorBlack: '#000000', + tooltipBg: '#ffffff', + tooltipBorderColor: '#c0c8d1', + tooltipExtraCss: 'border-radius: 10px; padding: 4px 16px;', + tooltipTextColor: '#2a2a2a', + tooltipFontWeight: 'bolder', + + lineBg: '#c0c8d1', + lineShadowBlur: '1', + itemColor: '#bcc3cc', + itemBorderColor: '#bcc3cc', + itemEmphasisBorderColor: '#42db7d', + shadowLineDarkBg: 'rgba(0, 0, 0, 0)', + shadowLineShadow: 'rgba(0, 0, 0, 0)', + gradFrom: '#ebeef2', + gradTo: '#ebeef2', + }, + + electricity: { + tooltipBg: '#ffffff', + tooltipLineColor: 'rgba(0, 0, 0, 0)', + tooltipLineWidth: '0', + tooltipBorderColor: '#ebeef2', + tooltipExtraCss: 'border-radius: 10px; padding: 8px 24px;', + tooltipTextColor: '#2a2a2a', + tooltipFontWeight: 'bolder', + + axisLineColor: 'rgba(0, 0, 0, 0)', + xAxisTextColor: '#2a2a2a', + yAxisSplitLine: '#ebeef2', + + itemBorderColor: '#42db7d', + lineStyle: 'solid', + lineWidth: '4', + lineGradFrom: '#42db7d', + lineGradTo: '#42db7d', + lineShadow: 'rgba(0, 0, 0, 0)', + + areaGradFrom: 'rgba(235, 238, 242, 0.5)', + areaGradTo: 'rgba(235, 238, 242, 0.5)', + shadowLineDarkBg: 'rgba(0, 0, 0, 0)', + }, + + bubbleMap: { + titleColor: '#484848', + areaColor: '#dddddd', + areaHoverColor: '#cccccc', + areaBorderColor: '#ebeef2', + }, + + echarts: { + bg: '#ffffff', + textColor: '#484848', + axisLineColor: '#bbbbbb', + splitLineColor: '#ebeef2', + itemHoverShadowColor: 'rgba(0, 0, 0, 0.5)', + tooltipBackgroundColor: '#6a7985', + areaOpacity: '0.7', + }, + + chartjs: { + axisLineColor: '#cccccc', + textColor: '#484848', + }, + }, +}; diff --git a/webapp/src/app/@theme/styles/themes.scss b/webapp/src/app/@theme/styles/themes.scss new file mode 100644 index 0000000..f5f75b6 --- /dev/null +++ b/webapp/src/app/@theme/styles/themes.scss @@ -0,0 +1,27 @@ +// @nebular theming framework +@import '~@nebular/theme/styles/theming'; +// @nebular out of the box themes +@import '~@nebular/theme/styles/themes'; + +// which themes you what to enable (empty to enable all) +$nb-enabled-themes: (default, cosmic); + +$nb-themes: nb-register-theme(( + // app wise variables for each theme + sidebar-header-gap: 2rem, + sidebar-header-height: initial, + layout-content-width: 1400px, + + font-main: Roboto, + font-secondary: Exo, +), default, default); + +$nb-themes: nb-register-theme(( + // app wise variables for each theme + sidebar-header-gap: 2rem, + sidebar-header-height: initial, + layout-content-width: 1400px, + + font-main: Roboto, + font-secondary: Exo, +), cosmic, cosmic); diff --git a/webapp/src/app/@theme/theme.module.ts b/webapp/src/app/@theme/theme.module.ts new file mode 100644 index 0000000..d786226 --- /dev/null +++ b/webapp/src/app/@theme/theme.module.ts @@ -0,0 +1,99 @@ +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { + NbActionsModule, + NbCardModule, + NbLayoutModule, + NbMenuModule, + NbRouteTabsetModule, + NbSearchModule, + NbSidebarModule, + NbTabsetModule, + NbThemeModule, + NbUserModule, + NbCheckboxModule, +} from '@nebular/theme'; + +import { + FooterComponent, + HeaderComponent, + SearchInputComponent, + ThemeSettingsComponent, + ThemeSwitcherComponent, + // XDS_MODS TinyMCEComponent, +} from './components'; +import { CapitalizePipe, PluralPipe, RoundPipe, TimingPipe } from './pipes'; +import { + OneColumnLayoutComponent, + SampleLayoutComponent, + XdsLayoutComponent, // XDS_MODS + ThreeColumnsLayoutComponent, + TwoColumnsLayoutComponent, +} from './layouts'; +import { DEFAULT_THEME } from './styles/theme.default'; +import { COSMIC_THEME } from './styles/theme.cosmic'; + +const BASE_MODULES = [CommonModule, FormsModule, ReactiveFormsModule]; + +const NB_MODULES = [ + NbCardModule, + NbLayoutModule, + NbTabsetModule, + NbRouteTabsetModule, + NbMenuModule, + NbUserModule, + NbActionsModule, + NbSearchModule, + NbSidebarModule, + NbCheckboxModule, + NgbModule, +]; + +const COMPONENTS = [ + ThemeSwitcherComponent, + HeaderComponent, + FooterComponent, + SearchInputComponent, + ThemeSettingsComponent, + // XDS_MODS TinyMCEComponent, + OneColumnLayoutComponent, + SampleLayoutComponent, + XdsLayoutComponent, // XDS_MODS + ThreeColumnsLayoutComponent, + TwoColumnsLayoutComponent, +]; + +const PIPES = [ + CapitalizePipe, + PluralPipe, + RoundPipe, + TimingPipe, +]; + +const NB_THEME_PROVIDERS = [ + ...NbThemeModule.forRoot( + { + name: 'default', // XDS_MODS + }, + [ DEFAULT_THEME, COSMIC_THEME ], + ).providers, + ...NbSidebarModule.forRoot().providers, + ...NbMenuModule.forRoot().providers, +]; + +@NgModule({ + imports: [...BASE_MODULES, ...NB_MODULES], + exports: [...BASE_MODULES, ...NB_MODULES, ...COMPONENTS, ...PIPES], + declarations: [...COMPONENTS, ...PIPES], +}) +export class ThemeModule { + static forRoot(): ModuleWithProviders { + return <ModuleWithProviders>{ + ngModule: ThemeModule, + providers: [...NB_THEME_PROVIDERS], + }; + } +} diff --git a/webapp/src/app/app-alert/app-alert.component.spec.ts b/webapp/src/app/app-alert/app-alert.component.spec.ts deleted file mode 100644 index 7f343dc..0000000 --- a/webapp/src/app/app-alert/app-alert.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { AppAlertComponent } from './app-alert.component'; - -describe('AppAlertComponent', () => { - let component: AppAlertComponent; - let fixture: ComponentFixture<AppAlertComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ AppAlertComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(AppAlertComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/webapp/src/app/app-alert/app-alert.component.ts b/webapp/src/app/app-alert/app-alert.component.ts deleted file mode 100644 index e6fbd47..0000000 --- a/webapp/src/app/app-alert/app-alert.component.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Component, ViewEncapsulation } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; - -import { AlertService, IAlert } from '../services/alert.service'; - -@Component({ - selector: 'app-alert', - template: ` - <div style="width:80%; margin-left:auto; margin-right:auto;" - *ngFor="let alert of (alerts$ | async)"> - <alert *ngIf="alert.show" [type]="alert.type" [dismissible]="alert.dismissible" [dismissOnTimeout]="alert.dismissTimeout" - (onClose)="onClose(alert)"> - <div style="text-align:center;" [innerHtml]="alert.msg"></div> - </alert> - </div> -`, -}) - -export class AppAlertComponent { - - alerts$: Observable<IAlert[]>; - - constructor(private alertSvr: AlertService) { - this.alerts$ = this.alertSvr.alerts; - } - - onClose(al) { - this.alertSvr.del(al); - } - -} diff --git a/webapp/src/app/app-routing.module.ts b/webapp/src/app/app-routing.module.ts index 36629de..5490a21 100644 --- a/webapp/src/app/app-routing.module.ts +++ b/webapp/src/app/app-routing.module.ts @@ -1,18 +1,57 @@ +import { ExtraOptions, RouterModule, Routes } from '@angular/router'; import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; -import { HomeComponent } from './home/home.component'; -import { ConfigComponent } from './config/config.component'; -import { DevelComponent } from './devel/devel.component'; +import { + NbAuthComponent, + NbLoginComponent, + NbLogoutComponent, + NbRegisterComponent, + NbRequestPasswordComponent, + NbResetPasswordComponent, +} from '@nebular/auth'; const routes: Routes = [ - { path: 'config', component: ConfigComponent, data: { title: 'Config' } }, - { path: 'home', component: HomeComponent, data: { title: 'Home' } }, - { path: 'devel', component: DevelComponent, data: { title: 'Build & Deploy' } }, - { path: '**', component: HomeComponent } + { path: 'pages', loadChildren: 'app/pages/pages.module#PagesModule' }, + { + path: 'auth', + component: NbAuthComponent, + children: [ + { + path: '', + component: NbLoginComponent, + }, + { + path: 'login', + component: NbLoginComponent, + }, + { + path: 'register', + component: NbRegisterComponent, + }, + { + path: 'logout', + component: NbLogoutComponent, + }, + { + path: 'request-password', + component: NbRequestPasswordComponent, + }, + { + path: 'reset-password', + component: NbResetPasswordComponent, + }, + ], + }, + { path: '', redirectTo: 'pages', pathMatch: 'full' }, + { path: '**', redirectTo: 'pages' }, ]; +const config: ExtraOptions = { + useHash: true, +}; + @NgModule({ - imports: [RouterModule.forRoot(routes)], - exports: [RouterModule] + imports: [RouterModule.forRoot(routes, config)], + exports: [RouterModule], }) -export class AppRoutingModule { } +export class AppRoutingModule { +} diff --git a/webapp/src/app/app-topnav/app-topnav.component.css b/webapp/src/app/app-topnav/app-topnav.component.css deleted file mode 100644 index a47ad13..0000000 --- a/webapp/src/app/app-topnav/app-topnav.component.css +++ /dev/null @@ -1,31 +0,0 @@ -.navbar { - background-color: whitesmoke; -} - -.navbar-brand { - font-size: x-large; - font-variant: small-caps; - color: #5a28a1; -} - -a.navbar-brand { - margin-top: 5px; -} - - -.navbar-nav ul li a { - color: #fff; -} - -.menu-text { - color: #fff; -} - -#logo-iot { - padding: 0 2px; - height: 60px; -} - -li>a { - color:#5a28a1; -} diff --git a/webapp/src/app/app-topnav/app-topnav.component.html b/webapp/src/app/app-topnav/app-topnav.component.html deleted file mode 100644 index be2dfa2..0000000 --- a/webapp/src/app/app-topnav/app-topnav.component.html +++ /dev/null @@ -1,24 +0,0 @@ -<nav class="navbar navbar-fixed-top"> - <!-- navbar-inverse"> --> - <div class="container-fluid"> - <div class="navbar-header"> - <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#myNavbar" - [attr.aria-expanded]="!isCollapsed" (click)="isCollapsed = !isCollapsed;" [ngClass]="{'collapsed': isCollapsed}"> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - <span class="icon-bar"></span> - </button> - - <img class="navbar-brand" id="logo-iot" src="assets/images/iot-bzh-logo-small.png"> - <a class="navbar-brand" href="#">X(cross) Development System Dashboard</a> - </div> - - <div class="collapse navbar-collapse" [ngClass]="{'in': !isCollapsed}" id="myNavbar"> - <ul class="nav navbar-nav navbar-right"> - <li><a routerLink="/config"><i class="fa fa-2x fa-cog" title="Open configuration page" (click)="isCollapsed=true;"></i></a></li> - <li><a routerLink="/devel"><i class="fa fa-2x fa-play-circle" title="Open build page" (click)="isCollapsed=true;"></i></a></li> - <li><a routerLink="/home"><i class="fa fa-2x fa-home" title="Back to home page" (click)="isCollapsed=true;"></i></a></li> - </ul> - </div> - </div> -</nav> diff --git a/webapp/src/app/app-topnav/app-topnav.component.ts b/webapp/src/app/app-topnav/app-topnav.component.ts deleted file mode 100644 index 9ba4021..0000000 --- a/webapp/src/app/app-topnav/app-topnav.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component, ViewEncapsulation } from '@angular/core'; - -@Component({ - selector: 'app-topnav', - templateUrl: './app-topnav.component.html', - styleUrls: ['./app-topnav.component.css'], - encapsulation: ViewEncapsulation.None -}) -export class AppTopnavComponent { - public isCollapsed = false; - - constructor() { } -} diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html deleted file mode 100644 index 6fcb46f..0000000 --- a/webapp/src/app/app.component.html +++ /dev/null @@ -1,7 +0,0 @@ -<app-topnav></app-topnav> - -<app-alert id="alert"></app-alert> - -<div style="margin:10px;"> - <router-outlet></router-outlet> -</div> diff --git a/webapp/src/app/app.component.ts b/webapp/src/app/app.component.ts index b644ecd..f858b0c 100644 --- a/webapp/src/app/app.component.ts +++ b/webapp/src/app/app.component.ts @@ -1,44 +1,21 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; -import { ConfigService, IConfig } from './services/config.service'; +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ +import { Component, OnInit } from '@angular/core'; +import { AnalyticsService } from './@core/utils/analytics.service'; @Component({ - selector: 'app-root', - templateUrl: 'app.component.html' + selector: 'ngx-app', + template: '<router-outlet></router-outlet>', }) +export class AppComponent implements OnInit { -export class AppComponent implements OnInit, OnDestroy { - private defaultLanguage = 'en'; - public isCollapsed = true; + constructor(private analytics: AnalyticsService) { + } - constructor(private translate: TranslateService, private configSvr: ConfigService) { - } - - ngOnInit() { - this.translate.addLangs(['en', 'fr']); - this.translate.setDefaultLang(this.defaultLanguage); - - const browserLang = this.translate.getBrowserLang(); - this.translate.use(browserLang.match(/en|fr/) ? browserLang : this.defaultLanguage); - - this.configSvr.Conf$.subscribe((cfg: IConfig) => { - let lang: string; - switch (cfg.language) { - case 'ENG': - lang = 'en'; - break; - case 'FRA': - lang = 'fr'; - break; - default: - lang = this.defaultLanguage; - } - this.translate.use(lang); - }); - } - - ngOnDestroy(): void { - // this.aglIdentityService.loginResponse.unsubscribe(); - // this.aglIdentityService.logoutResponse.unsubscribe(); - } + ngOnInit(): void { + this.analytics.trackPageViews(); + } } diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts index bf63b5e..9d992e7 100644 --- a/webapp/src/app/app.module.ts +++ b/webapp/src/app/app.module.ts @@ -1,110 +1,36 @@ -import { NgModule } from '@angular/core'; -import { HttpClientModule, HttpClient } from '@angular/common/http'; +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ +import { APP_BASE_HREF } from '@angular/common'; import { BrowserModule } from '@angular/platform-browser'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; -import { TranslateHttpLoader } from '@ngx-translate/http-loader'; -import { FileUploadModule } from 'ng2-file-upload'; -import { LocationStrategy, HashLocationStrategy } from '@angular/common'; -import { CookieModule } from 'ngx-cookie'; - -// Import bootstrap -import { AlertModule } from 'ngx-bootstrap/alert'; -import { ModalModule } from 'ngx-bootstrap/modal'; -import { AccordionModule } from 'ngx-bootstrap/accordion'; -import { CarouselModule } from 'ngx-bootstrap/carousel'; -import { PopoverModule } from 'ngx-bootstrap/popover'; -import { CollapseModule } from 'ngx-bootstrap/collapse'; -import { BsDropdownModule } from 'ngx-bootstrap/dropdown'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NgModule } from '@angular/core'; +import { HttpModule } from '@angular/http'; +import { CoreXdsModule } from './@core-xds/core-xds.module'; -// Import the application components and services. -import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { AppTopnavComponent } from './app-topnav/app-topnav.component'; -import { AppAlertComponent } from './app-alert/app-alert.component'; -import { HomeComponent } from './home/home.component'; -import { ConfigComponent } from './config/config.component'; -import { DwnlAgentComponent } from './config/downloadXdsAgent.component'; -import { DevelComponent } from './devel/devel.component'; -import { BuildComponent } from './devel/build/build.component'; -import { ProjectCardComponent } from './projects/projectCard.component'; -import { ProjectReadableTypePipe } from './projects/projectCard.component'; -import { ProjectsListAccordionComponent } from './projects/projectsListAccordion.component'; -import { ProjectAddModalComponent } from './projects/projectAddModal.component'; -import { SdkCardComponent } from './sdks/sdkCard.component'; -import { SdksListAccordionComponent } from './sdks/sdksListAccordion.component'; -import { SdkSelectDropdownComponent } from './sdks/sdkSelectDropdown.component'; -import { SdkAddModalComponent } from './sdks/sdkAddModal.component'; - -import { AlertService } from './services/alert.service'; -import { ConfigService } from './services/config.service'; -import { ProjectService } from './services/project.service'; -import { SdkService } from './services/sdk.service'; -import { UtilsService } from './services/utils.service'; -import { XDSAgentService } from './services/xdsagent.service'; - -import { SafePipe } from './common/safe.pipe'; - -export function createTranslateLoader(http: HttpClient) { - return new TranslateHttpLoader(http, './assets/i18n/', '.json'); -} +import { AppRoutingModule } from './app-routing.module'; +import { ThemeModule } from './@theme/theme.module'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; @NgModule({ - imports: [ - BrowserModule, - FormsModule, - ReactiveFormsModule, - HttpClientModule, - AppRoutingModule, - FileUploadModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useFactory: (createTranslateLoader), - deps: [HttpClient] - } - }), - CookieModule.forRoot(), - AlertModule.forRoot(), - ModalModule.forRoot(), - AccordionModule.forRoot(), - CarouselModule.forRoot(), - PopoverModule.forRoot(), - CollapseModule.forRoot(), - BsDropdownModule.forRoot(), - ], - declarations: [ - AppComponent, - AppTopnavComponent, - AppAlertComponent, - HomeComponent, - ConfigComponent, - DwnlAgentComponent, - DevelComponent, - BuildComponent, - ProjectCardComponent, - ProjectReadableTypePipe, - ProjectsListAccordionComponent, - ProjectAddModalComponent, - SdkCardComponent, - SdksListAccordionComponent, - SdkSelectDropdownComponent, - SdkAddModalComponent, - SafePipe, - ], - providers: [ - { - provide: LocationStrategy, useClass: HashLocationStrategy, - }, - AlertService, - ConfigService, - ProjectService, - SdkService, - UtilsService, - XDSAgentService - ], - bootstrap: [AppComponent] + declarations: [AppComponent], + imports: [ + BrowserModule, + BrowserAnimationsModule, + HttpModule, + AppRoutingModule, + + NgbModule.forRoot(), + ThemeModule.forRoot(), + CoreXdsModule.forRoot(), + ], + bootstrap: [AppComponent], + providers: [ + { provide: APP_BASE_HREF, useValue: '/' }, + ], }) export class AppModule { - constructor() { } } diff --git a/webapp/src/app/common/safe.pipe.ts b/webapp/src/app/common/safe.pipe.ts deleted file mode 100644 index 84fd6b5..0000000 --- a/webapp/src/app/common/safe.pipe.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; - -@Pipe({ name: 'safe' }) -export class SafePipe implements PipeTransform { - constructor(private sanitizer: DomSanitizer) { } - transform(url) { - return this.sanitizer.bypassSecurityTrustResourceUrl(url); - } -} diff --git a/webapp/src/app/config/config.component.css b/webapp/src/app/config/config.component.css deleted file mode 100644 index 6412f9a..0000000 --- a/webapp/src/app/config/config.component.css +++ /dev/null @@ -1,35 +0,0 @@ -.fa-big { - font-size: 20px; - font-weight: bold; -} - -.fa-size-x2 { - font-size: 20px; -} - -h2 { - font-family: sans-serif; - font-variant: small-caps; - font-size: x-large; -} - -th span { - font-weight: 100; -} - -th label { - font-weight: 100; - margin-bottom: 0; -} - -tr.info>th { - vertical-align: middle; -} - -tr.info>td { - vertical-align: middle; -} - -.panel-heading { - background: aliceblue; -} diff --git a/webapp/src/app/config/config.component.html b/webapp/src/app/config/config.component.html deleted file mode 100644 index ba3bd72..0000000 --- a/webapp/src/app/config/config.component.html +++ /dev/null @@ -1,101 +0,0 @@ -<div class="panel panel-default"> - <div class="panel-heading"> - <h2 class="panel-title" (click)="gConfigIsCollapsed = !gConfigIsCollapsed"> - Global Configuration - <div class="pull-right"> - <span class="fa fa-fw fa-exchange fa-size-x2" [style.color]="((agentStatus$ | async)?.WS_connected)?'green':'red'"></span> - - <button class="btn btn-link" (click)="gConfigIsCollapsed = !gConfigIsCollapsed; $event.stopPropagation()"> - <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': gConfigIsCollapsed, 'fa-angle-double-right': !gConfigIsCollapsed}"></span> - </button> - </div> - </h2> - </div> - <div class="panel-body" [collapse]="gConfigIsCollapsed && xdsServerConnected"> - <div class="row"> - <div class="col-xs-12"> - <table class="table table-condensed"> - <tbody> - <tr [ngClass]="{'info': xdsServerConnected, 'danger': !xdsServerConnected}"> - <th><label>XDS Server URL</label></th> - <td> <input type="text" [(ngModel)]="xdsServerUrl"></td> - <td style="white-space: nowrap"> - <div class="btn-group"> - <button class="btn btn-link" (click)="xdsAgentRestartConn()"><span class="fa fa-refresh fa-size-x2"></span></button> - <xds-dwnl-agent class="button"></xds-dwnl-agent> - </div> - </td> - </tr> - <tr class="info"> - <th><label>XDS Server connection retry</label></th> - <td> <input type="text" [(ngModel)]="xdsServerRetry" (ngModelChange)="showApplyBtn['retry'] = true"></td> - <td> - <button *ngIf="showApplyBtn['retry']" class="btn btn-primary btn-xs" (click)="submitGlobConf('retry')">APPLY</button> - </td> - </tr> - <tr class="info"> - <th><label>Local Projects root directory</label></th> - <td> <input type="text" [(ngModel)]="projectsRootDir" (ngModelChange)="showApplyBtn['rootDir'] = true"></td> - <td> - <button *ngIf="showApplyBtn['rootDir']" class="btn btn-primary btn-xs" (click)="submitGlobConf('rootDir')">APPLY</button> - </td> - </tr> - </tbody> - </table> - </div> - </div> - </div> -</div> - -<div class="panel panel-default"> - <div class="panel-heading"> - <h2 class="panel-title" (click)="sdksIsCollapsed = !sdksIsCollapsed"> - Cross SDKs - <div class="pull-right"> - <button class="btn btn-link" (click)="childSdkModal.show(); $event.stopPropagation()"><span class="fa fa-plus fa-size-x2"></span></button> - - <button class="btn btn-link" (click)="sdksIsCollapsed = !sdksIsCollapsed; $event.stopPropagation()"> - <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': sdksIsCollapsed, 'fa-angle-double-right': !sdksIsCollapsed}"></span> - </button> - </div> - </h2> - </div> - <div class="panel-body" [collapse]="sdksIsCollapsed"> - <div class="row col-xs-12"> - <xds-sdks-list-accordion [sdks]="(sdks$ | async)"></xds-sdks-list-accordion> - </div> - </div> -</div> - -<div class="panel panel-default"> - <div class="panel-heading"> - <h2 class="panel-title" (click)="projectsIsCollapsed = !projectsIsCollapsed; $event.stopPropagation()"> - Projects - <div class="pull-right"> - <button class="btn btn-link" (click)="childProjectModal.show(); $event.stopPropagation()"><span class="fa fa-plus fa-size-x2"></span></button> - - <button class="btn btn-link" (click)="projectsIsCollapsed = !projectsIsCollapsed; $event.stopPropagation()"> - <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': projectsIsCollapsed, 'fa-angle-double-right': !projectsIsCollapsed}"></span> - </button> - </div> - </h2> - </div> - <div class="panel-body" [collapse]="projectsIsCollapsed"> - <div class="row col-xs-12"> - <xds-projects-list-accordion [projects]="(projects$ | async)"></xds-projects-list-accordion> - </div> - </div> -</div> - -<!-- Modals --> -<xds-project-add-modal #childProjectModal [title]="'Add a new project'" [server-id]=curServerID> -</xds-project-add-modal> -<xds-sdk-add-modal #childSdkModal [title]="'Add a new SDK'"> -</xds-sdk-add-modal> - -<!-- only for debug --> -<div *ngIf="false" class="row"> - <pre>Config: {{config$ | async | json}}</pre> - <br> - <pre>Projects: {{projects$ | async | json}} </pre> -</div> diff --git a/webapp/src/app/config/config.component.spec.ts b/webapp/src/app/config/config.component.spec.ts deleted file mode 100644 index ec5d3be..0000000 --- a/webapp/src/app/config/config.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ConfigComponent } from './config.component'; - -describe('ConfigComponent', () => { - let component: ConfigComponent; - let fixture: ComponentFixture<ConfigComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ ConfigComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ConfigComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/webapp/src/app/config/config.component.ts b/webapp/src/app/config/config.component.ts deleted file mode 100644 index 3db7f60..0000000 --- a/webapp/src/app/config/config.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { CollapseModule } from 'ngx-bootstrap/collapse'; - -import { ConfigService, IConfig } from '../services/config.service'; -import { ProjectService, IProject } from '../services/project.service'; -import { XDSAgentService, IAgentStatus, IXDSConfig } from '../services/xdsagent.service'; -import { AlertService } from '../services/alert.service'; -import { ProjectAddModalComponent } from '../projects/projectAddModal.component'; -import { SdkService, ISdk } from '../services/sdk.service'; -import { SdkAddModalComponent } from '../sdks/sdkAddModal.component'; - -@Component({ - selector: 'app-config', - templateUrl: './config.component.html', - styleUrls: ['./config.component.css'], - encapsulation: ViewEncapsulation.None -}) - -// Inspired from https://embed.plnkr.co/jgDTXknPzAaqcg9XA9zq/ -// and from http://plnkr.co/edit/vCdjZM?p=preview - -export class ConfigComponent implements OnInit { - @ViewChild('childProjectModal') childProjectModal: ProjectAddModalComponent; - @ViewChild('childSdkModal') childSdkModal: SdkAddModalComponent; - - config$: Observable<IConfig>; - projects$: Observable<IProject[]>; - sdks$: Observable<ISdk[]>; - agentStatus$: Observable<IAgentStatus>; - - curProj: number; - curServer: number; - curServerID: string; - userEditedLabel = false; - - gConfigIsCollapsed = true; - sdksIsCollapsed = true; - projectsIsCollapsed = false; - - // TODO replace by reactive FormControl + add validation - xdsServerConnected = false; - xdsServerUrl: string; - xdsServerRetry: string; - projectsRootDir: string; // FIXME: should be remove when projectAddModal will always return full path - showApplyBtn = { // Used to show/hide Apply buttons - 'retry': false, - 'rootDir': false, - }; - - constructor( - private configSvr: ConfigService, - private projectSvr: ProjectService, - private xdsAgentSvr: XDSAgentService, - private sdkSvr: SdkService, - private alert: AlertService, - ) { - } - - ngOnInit() { - this.config$ = this.configSvr.Conf$; - this.projects$ = this.projectSvr.Projects$; - this.sdks$ = this.sdkSvr.Sdks$; - this.agentStatus$ = this.xdsAgentSvr.Status$; - - // FIXME support multiple servers - this.curServer = 0; - - // Bind xdsServerUrl to baseURL - this.xdsAgentSvr.XdsConfig$.subscribe(cfg => { - if (!cfg || cfg.servers.length < 1) { - return; - } - const svr = cfg.servers[this.curServer]; - this.curServerID = svr.id; - this.xdsServerConnected = svr.connected; - this.xdsServerUrl = svr.url; - this.xdsServerRetry = String(svr.connRetry); - this.projectsRootDir = ''; // SEB FIXME: add in go config? cfg.projectsRootDir; - }); - } - - submitGlobConf(field: string) { - switch (field) { - case 'retry': - const re = new RegExp('^[0-9]+$'); - const rr = parseInt(this.xdsServerRetry, 10); - if (re.test(this.xdsServerRetry) && rr >= 0) { - this.xdsAgentSvr.setServerRetry(this.curServerID, rr); - } else { - this.alert.warning('Not a valid number', true); - } - break; - case 'rootDir': - this.configSvr.projectsRootDir = this.projectsRootDir; - break; - default: - return; - } - this.showApplyBtn[field] = false; - } - - xdsAgentRestartConn() { - const url = this.xdsServerUrl; - this.xdsAgentSvr.setServerUrl(this.curServerID, url); - } - -} diff --git a/webapp/src/app/devel/build/build.component.css b/webapp/src/app/devel/build/build.component.css deleted file mode 100644 index 695a89b..0000000 --- a/webapp/src/app/devel/build/build.component.css +++ /dev/null @@ -1,54 +0,0 @@ -.vcenter { - display: inline-block; - vertical-align: middle; -} - -.blocks .btn-primary { - margin-left: 5px; - margin-right: 5px; - margin-top: 5px; - border-radius: 4px !important; -} - -.table-center { - width: 80%; - margin-left: auto; - margin-right: auto; -} - -.table-borderless>tbody>tr>td, -.table-borderless>tbody>tr>th, -.table-borderless>tfoot>tr>td, -.table-borderless>tfoot>tr>th, -.table-borderless>thead>tr>td, -.table-borderless>thead>tr>th { - border: none; -} - -.table-in-accordion>tbody>tr>th { - width: 30% -} - -.btn-large { - width: 10em; -} - -.fa-big { - font-size: 18px; - font-weight: bold; -} - -.textarea-scroll { - width: 100%; - overflow-y: scroll; -} - -h2 { - font-family: sans-serif; - font-variant: small-caps; - font-size: x-large; -} - -.panel-heading { - background: aliceblue; -} diff --git a/webapp/src/app/devel/build/build.component.html b/webapp/src/app/devel/build/build.component.html deleted file mode 100644 index 0cf0290..0000000 --- a/webapp/src/app/devel/build/build.component.html +++ /dev/null @@ -1,114 +0,0 @@ -<div class="panel panel-default"> - <div class="panel-heading"> - <h2 class="panel-title" (click)="buildIsCollapsed = !buildIsCollapsed"> - Build - <div class="pull-right"> - <button class="btn btn-link" (click)="buildIsCollapsed = !buildIsCollapsed; $event.stopPropagation()"> - <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': buildIsCollapsed, 'fa-angle-double-right': !buildIsCollapsed}"></span> - </button> - </div> - </h2> - </div> - <div class="panel-body" [collapse]="buildIsCollapsed"> - <form [formGroup]="buildForm"> - <div class="col-xs-12"> - <table class="table table-borderless table-center"> - <tbody> - <tr> - <th>Cross SDK</th> - <td> - <!-- FIXME why not working ? - <xds-sdk-select-dropdown [sdks]="(sdks$ | async)"></xds-sdk-select-dropdown> - --> - <xds-sdk-select-dropdown></xds-sdk-select-dropdown> - </td> - </tr> - <tr> - <th>Project root path</th> - <td> <input type="text" disabled style="width:99%;" [value]="curProject && curProject.pathClient"></td> - </tr> - <tr> - <th>Sub-path</th> - <td> <input type="text" style="width:99%;" formControlName="subpath"> </td> - </tr> - <tr> - <td colspan="2"> - <accordion> - <accordion-group #group> - <div accordion-heading> - Advanced Settings - <i class="pull-right float-xs-right fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i> - </div> - - <table class="table table-borderless table-in-accordion"> - <tbody> - <tr> - <th>Clean Command</th> - <td> <input type="text" style="width:99%;" formControlName="cmdClean"> </td> - </tr> - <tr> - <th>Pre-Build Command</th> - <td> <input type="text" style="width:99%;" formControlName="cmdPrebuild"> </td> - </tr> - <tr> - <th>Build Command</th> - <td> <input type="text" style="width:99%;" formControlName="cmdBuild"> </td> - </tr> - <tr> - <th>Populate Command</th> - <td> <input type="text" style="width:99%;" formControlName="cmdPopulate"> </td> - </tr> - <tr> - <th>Env variables</th> - <td> <input type="text" style="width:99%;" formControlName="envVars"> </td> - </tr> - <tr *ngIf="debugEnable"> - <th>Args variables</th> - <td> <input type="text" style="width:99%;" formControlName="cmdArgs"> </td> - </tr> - </tbody> - </table> - </accordion-group> - </accordion> - </td> - </tr> - </tbody> - </table> - </div> - <div class="row"> - <div class="col-xs-12 text-center"> - <div class="btn-group blocks"> - <button class="btn btn-primary btn-large" (click)="clean()" [disabled]="!curProject ">Clean</button> - <button class="btn btn-primary btn-large" (click)="preBuild()" [disabled]="!curProject">Pre-Build</button> - <button class="btn btn-primary btn-large" (click)="build()" [disabled]="!curProject">Build</button> - <button class="btn btn-primary btn-large" (click)="populate()" [disabled]="!curProject ">Populate</button> - <button *ngIf="debugEnable" class="btn btn-primary btn-large" (click)="execCmd()" [disabled]="!curProject ">Execute command</button> - </div> - </div> - </div> - </form> - - <div style="margin-left: 2em; margin-right: 2em; "> - <div class="row "> - <div class="col-xs-10"> - <div class="row "> - <div class="col-xs-4"> - <label>Command Output</label> - </div> - <div class="col-xs-8" style="font-size:x-small; margin-top:5px;"> - {{ cmdInfo }} - </div> - </div> - </div> - <div class="col-xs-2"> - <button class="btn btn-link pull-right " (click)="reset() "><span class="fa fa-eraser fa-size-x2"></span></button> - </div> - </div> - <div class="row "> - <div class="col-xs-12 text-center "> - <textarea rows="20" class="textarea-scroll" #scrollOutput>{{ cmdOutput }}</textarea> - </div> - </div> - </div> - </div> -</div> diff --git a/webapp/src/app/devel/build/build.component.ts b/webapp/src/app/devel/build/build.component.ts deleted file mode 100644 index 49f42eb..0000000 --- a/webapp/src/app/devel/build/build.component.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { Component, ViewEncapsulation, AfterViewChecked, ElementRef, ViewChild, OnInit, Input } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms'; -import { CookieService } from 'ngx-cookie'; - -import 'rxjs/add/operator/scan'; -import 'rxjs/add/operator/startWith'; - -import { XDSAgentService, ICmdOutput } from '../../services/xdsagent.service'; -import { ProjectService, IProject } from '../../services/project.service'; -import { AlertService, IAlert } from '../../services/alert.service'; -import { SdkService } from '../../services/sdk.service'; - -@Component({ - selector: 'xds-panel-build', - templateUrl: './build.component.html', - styleUrls: ['./build.component.css'], -encapsulation: ViewEncapsulation.None -}) - -export class BuildComponent implements OnInit, AfterViewChecked { - @ViewChild('scrollOutput') private scrollContainer: ElementRef; - - @Input() curProject: IProject; - - public buildForm: FormGroup; - public subpathCtrl = new FormControl('', Validators.required); - public debugEnable = false; - public buildIsCollapsed = false; - public cmdOutput: string; - public cmdInfo: string; - - private startTime: Map<string, number> = new Map<string, number>(); - - constructor( - private xdsSvr: XDSAgentService, - private fb: FormBuilder, - private alertSvr: AlertService, - private sdkSvr: SdkService, - private cookie: CookieService, - ) { - this.cmdOutput = ''; - this.cmdInfo = ''; // TODO: to be remove (only for debug) - this.buildForm = fb.group({ - subpath: this.subpathCtrl, - cmdClean: ['', Validators.nullValidator], - cmdPrebuild: ['', Validators.nullValidator], - cmdBuild: ['', Validators.nullValidator], - cmdPopulate: ['', Validators.nullValidator], - cmdArgs: ['', Validators.nullValidator], - envVars: ['', Validators.nullValidator], - }); - } - - ngOnInit() { - // Set default settings - // TODO save & restore values from cookies - this.buildForm.patchValue({ - subpath: '', - cmdClean: 'rm -rf build', - cmdPrebuild: 'mkdir -p build && cd build && cmake ..', - cmdBuild: 'cd build && make', - cmdPopulate: 'cd build && make remote-target-populate', - cmdArgs: '', - envVars: '', - }); - - // Command output data tunneling - this.xdsSvr.CmdOutput$.subscribe(data => { - this.cmdOutput += data.stdout; - this.cmdOutput += data.stderr; - }); - - // Command exit - this.xdsSvr.CmdExit$.subscribe(exit => { - if (this.startTime.has(exit.cmdID)) { - this.cmdInfo = 'Last command duration: ' + this._computeTime(this.startTime.get(exit.cmdID)); - this.startTime.delete(exit.cmdID); - } - - if (exit && exit.code !== 0) { - this.cmdOutput += '--- Command exited with code ' + exit.code + ' ---\n\n'; - } - }); - - this._scrollToBottom(); - - // only use for debug - this.debugEnable = (this.cookie.get('debug_build') === '1'); - } - - ngAfterViewChecked() { - this._scrollToBottom(); - } - - reset() { - this.cmdOutput = ''; - } - - clean() { - this._exec( - this.buildForm.value.cmdClean, - this.buildForm.value.subpath, - [], - this.buildForm.value.envVars); - } - - preBuild() { - this._exec( - this.buildForm.value.cmdPrebuild, - this.buildForm.value.subpath, - [], - this.buildForm.value.envVars); - } - - build() { - this._exec( - this.buildForm.value.cmdBuild, - this.buildForm.value.subpath, - [], - this.buildForm.value.envVars - ); - } - - populate() { - this._exec( - this.buildForm.value.cmdPopulate, - this.buildForm.value.subpath, - [], // args - this.buildForm.value.envVars - ); - } - - execCmd() { - this._exec( - this.buildForm.value.cmdArgs, - this.buildForm.value.subpath, - [], - this.buildForm.value.envVars - ); - } - - private _exec(cmd: string, dir: string, args: string[], env: string) { - if (!this.curProject) { - this.alertSvr.warning('No active project', true); - } - - const prjID = this.curProject.id; - - this.cmdOutput += this._outputHeader(); - - const sdkid = this.sdkSvr.getCurrentId(); - - // Detect key=value in env string to build array of string - const envArr = []; - env.split(';').forEach(v => envArr.push(v.trim())); - - const t0 = performance.now(); - this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0; - - this.xdsSvr.exec(prjID, dir, cmd, sdkid, args, envArr) - .subscribe(res => { - this.startTime.set(String(res.cmdID), t0); - }, - err => { - this.cmdInfo = 'Last command duration: ' + this._computeTime(t0); - this.alertSvr.error('ERROR: ' + err); - }); - } - - private _scrollToBottom(): void { - try { - this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight; - } catch (err) { } - } - - private _computeTime(t0: number, t1?: number): string { - const enlap = Math.round((t1 || performance.now()) - t0); - if (enlap < 1000.0) { - return enlap.toFixed(2) + ' ms'; - } else { - return (enlap / 1000.0).toFixed(3) + ' seconds'; - } - } - - private _outputHeader(): string { - return '--- ' + new Date().toString() + ' ---\n'; - } - - private _outputFooter(): string { - return '\n'; - } -} diff --git a/webapp/src/app/devel/devel.component.css b/webapp/src/app/devel/devel.component.css deleted file mode 100644 index 4b03dcb..0000000 --- a/webapp/src/app/devel/devel.component.css +++ /dev/null @@ -1,19 +0,0 @@ -.table-center { - width: 60%; - margin-left: auto; - margin-right: auto; -} - -.table-borderless>tbody>tr>td, -.table-borderless>tbody>tr>th, -.table-borderless>tfoot>tr>td, -.table-borderless>tfoot>tr>th, -.table-borderless>thead>tr>td, -.table-borderless>thead>tr>th { - border: none; -} - -a.dropdown-item.disabled { - pointer-events:none; - opacity:0.4; -} diff --git a/webapp/src/app/devel/devel.component.html b/webapp/src/app/devel/devel.component.html deleted file mode 100644 index 495eed4..0000000 --- a/webapp/src/app/devel/devel.component.html +++ /dev/null @@ -1,40 +0,0 @@ -<div class="row"> - <div class="col-md-8"> - <table class="table table-borderless table-center"> - <tbody> - <tr> - <th style="border: none;">Project</th> - <td> - <div class="btn-group" dropdown *ngIf="curPrj"> - <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;"> - {{curPrj.label}} - <span class="caret" style="float: right; margin-top: 8px;"></span> - </button> - <ul *dropdownMenu class="dropdown-menu" role="menu"> - <li role="menuitem"><a class="dropdown-item" *ngFor="let prj of (Prjs$ | async)" [class.disabled]="!prj.isUsable" - (click)="curPrj=prj">{{prj.label}}</a> - </li> - - </ul> - </div> - <span *ngIf="!curPrj" style="color:red; font-style: italic;"> - No project detected, please create first a project using the configuration page. - </span> - </td> - </tr> - </tbody> - </table> - </div> -</div> - -<div class="row"> - <!--<div class="col-md-8">--> - <div class="col-md-12"> - <xds-panel-build [curProject]=curPrj></xds-panel-build> - </div> - <!-- TODO: disable for now - <div class="col-md-4"> - <panel-deploy [curProject]=curPrj></panel-deploy> - </div> - --> -</div> diff --git a/webapp/src/app/devel/devel.component.spec.ts b/webapp/src/app/devel/devel.component.spec.ts deleted file mode 100644 index 6483bf5..0000000 --- a/webapp/src/app/devel/devel.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DevelComponent } from './devel.component'; - -describe('DevelComponent', () => { - let component: DevelComponent; - let fixture: ComponentFixture<DevelComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DevelComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DevelComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/webapp/src/app/devel/devel.component.ts b/webapp/src/app/devel/devel.component.ts deleted file mode 100644 index eda03ef..0000000 --- a/webapp/src/app/devel/devel.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Component, OnInit, ViewEncapsulation } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; - -import { ProjectService, IProject } from '../services/project.service'; - -@Component({ - selector: 'xds-devel', - templateUrl: './devel.component.html', - styleUrls: ['./devel.component.css'], - encapsulation: ViewEncapsulation.None -}) - -export class DevelComponent implements OnInit { - - curPrj: IProject; - Prjs$: Observable<IProject[]>; - - constructor(private projectSvr: ProjectService) { - } - - ngOnInit() { - this.Prjs$ = this.projectSvr.Projects$; - this.Prjs$.subscribe((prjs) => { - // Select project if no one is selected or no project exists - if (this.curPrj && 'id' in this.curPrj) { - this.curPrj = prjs.find(p => p.id === this.curPrj.id) || prjs[0]; - } else if (this.curPrj == null) { - this.curPrj = prjs[0]; - } else { - this.curPrj = null; - } - }); - } -} diff --git a/webapp/src/app/home/home.component.css b/webapp/src/app/home/home.component.css deleted file mode 100644 index 7831443..0000000 --- a/webapp/src/app/home/home.component.css +++ /dev/null @@ -1,18 +0,0 @@ -.wide img { - width: 98%; -} -.carousel-item { - max-height: 90%; -} -h1, h2, h3, h4, p { - color: #330066; -} -.html-inner { - color: #330066; -} -h1 { - font-size: 4em; -} -p { - font-size: 2.5em; -} diff --git a/webapp/src/app/home/home.component.html b/webapp/src/app/home/home.component.html deleted file mode 100644 index 568fb9b..0000000 --- a/webapp/src/app/home/home.component.html +++ /dev/null @@ -1,13 +0,0 @@ -<div class="wide"> - <carousel [interval]="carInterval" [(activeSlide)]="activeSlideIndex"> - <slide *ngFor="let sl of slides; let index=index"> - <img [src]="sl.img" [alt]="sl.imgAlt"> - <div class="carousel-caption"> - <h1 *ngIf="sl.hText">{{ sl.hText }}</h1> - <h1 *ngIf="sl.hHtml" class="html-inner" [innerHtml]="sl.hHtml"></h1> - <p *ngIf="sl.text">{{ sl.text }}</p> - <div *ngIf="sl.html" class="html-inner" [innerHtml]="sl.html"></div> - </div> - </slide> - </carousel> -</div> diff --git a/webapp/src/app/home/home.component.ts b/webapp/src/app/home/home.component.ts deleted file mode 100644 index 66a2f30..0000000 --- a/webapp/src/app/home/home.component.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -export interface ISlide { - img?: string; - imgAlt?: string; - hText?: string; - hHtml?: string; - text?: string; - html?: string; - btn?: string; - btnHref?: string; -} - -@Component({ - selector: 'xds-home', - templateUrl: 'home.component.html', - styleUrls: ['home.component.css'] -}) - -export class HomeComponent { - - public carInterval = 4000; - public activeSlideIndex = 0; - - // FIXME SEB - Add more slides and info - public slides: ISlide[] = [ - { - img: 'assets/images/iot-graphx.jpg', - imgAlt: 'iot graphx image', - hText: 'Welcome to XDS Dashboard !', - text: 'X(cross) Development System allows developers to easily cross-compile applications.', - }, - { - img: 'assets/images/iot-graphx.jpg', - imgAlt: 'iot graphx image', - hText: 'Create, Build, Deploy, Enjoy !', - }, - { - img: 'assets/images/iot-graphx.jpg', - imgAlt: 'iot graphx image', - hHtml: '<p>To Start: click on <i class=\'fa fa-cog\' style=\'color:#9d9d9d;\'></i> icon and add new folder</p>', - } - ]; - - constructor() { } -} diff --git a/webapp/src/app/pages/build/build.component.html b/webapp/src/app/pages/build/build.component.html new file mode 100644 index 0000000..a1ef62d --- /dev/null +++ b/webapp/src/app/pages/build/build.component.html @@ -0,0 +1,63 @@ +<div class="row"> + <div class="col-12"> + <nb-card-body> + <nb-actions size="medium"> + <nb-action> + <xds-project-select-dropdown></xds-project-select-dropdown> + </nb-action> + <nb-action> + <xds-sdk-select-dropdown></xds-sdk-select-dropdown> + </nb-action> + </nb-actions> + </nb-card-body> + </div> + <div class="col-md-12 col-lg-12 col-xxxl-6"> + <nb-card size="xlarge"> + <nb-tabset fullWidth> + + <nb-tab tabTitle="Build"> + + <div class="row" style="margin-top:1em;"> + <!-- FIXME SEB + <button class="btn pull-right " (click)="reset() "> + <span class="fa fa-eraser fa-size-x2"></span> + </button> + --> + <div class="col-md-12 text-center "> + <textarea rows="20" class="textarea-scroll" #scrollOutput>{{ cmdOutput }}</textarea> + </div> + </div> + + <nb-card-body> + <nb-actions size="medium" fullWidth> + <nb-action (click)="clean()"> + <i class="fa fa-eraser"></i> + <span>Clean</span> + </nb-action> + <nb-action (click)="preBuild()"> + <i class="nb-list"></i> + <span>Pre-Build</span> + </nb-action> + <nb-action (click)="build()"> + <i class="fa fa-wrench"></i> + <span>Build</span> + </nb-action> + <nb-action (click)="populate()"> + <i class="fa fa-send"></i> + <span>Populate</span> + </nb-action> + </nb-actions> + </nb-card-body> + + </nb-tab> + + <nb-tab tabTitle="Deploy"> + <span> Content deploy...</span> + </nb-tab> + <nb-tab tabTitle="Debug"> + <span> Content debug...</span> + </nb-tab> + </nb-tabset> + </nb-card> + </div> +</div> diff --git a/webapp/src/app/pages/build/build.component.scss b/webapp/src/app/pages/build/build.component.scss new file mode 100644 index 0000000..b256f66 --- /dev/null +++ b/webapp/src/app/pages/build/build.component.scss @@ -0,0 +1,37 @@ +@import '../../@theme/styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; +@include nb-install-component() { + nb-tabset { + height: 100%; + display: flex; + flex-direction: column; + } + nb-tab { + padding: nb-theme(padding); + } + /deep/ ngx-tab1, + /deep/ ngx-tab2 { + display: block; + padding: nb-theme(padding); + } + @include media-breakpoint-down(xs) { + nb-tabset /deep/ul { + font-size: 1rem; + padding: 0 0.25rem; + } + } +} + +nb-action { + i { + font-size: 2rem; + margin-right: 0.5rem; + } +} + +.textarea-scroll { + border-color: lightgray; + width: 97%; + overflow-y: scroll; +} diff --git a/webapp/src/app/devel/build/build.component.spec.ts b/webapp/src/app/pages/build/build.component.spec.ts index 016192c..016192c 100644 --- a/webapp/src/app/devel/build/build.component.spec.ts +++ b/webapp/src/app/pages/build/build.component.spec.ts diff --git a/webapp/src/app/pages/build/build.component.ts b/webapp/src/app/pages/build/build.component.ts new file mode 100644 index 0000000..5adb9bc --- /dev/null +++ b/webapp/src/app/pages/build/build.component.ts @@ -0,0 +1,198 @@ +import { Component, ViewEncapsulation, AfterViewChecked, ElementRef, ViewChild, OnInit, Input } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms'; +import { CookieService } from 'ngx-cookie'; + +import 'rxjs/add/operator/scan'; +import 'rxjs/add/operator/startWith'; + +import { XDSAgentService, ICmdOutput } from '../../@core-xds/services/xdsagent.service'; +import { ProjectService, IProject } from '../../@core-xds/services/project.service'; +import { AlertService, IAlert } from '../../@core-xds/services/alert.service'; +import { SdkService } from '../../@core-xds/services/sdk.service'; + +@Component({ + selector: 'xds-panel-build', + templateUrl: './build.component.html', + styleUrls: ['./build.component.scss'], + encapsulation: ViewEncapsulation.None, +}) + +export class BuildComponent implements OnInit, AfterViewChecked { + @ViewChild('scrollOutput') private scrollContainer: ElementRef; + + // FIXME workaround of https://github.com/angular/angular-cli/issues/2034 + // should be removed with angular 5 + // @Input() curProject: IProject; + @Input() curProject = <IProject>null; + + public buildForm: FormGroup; + public subpathCtrl = new FormControl('', Validators.required); + public debugEnable = false; + public buildIsCollapsed = false; + public cmdOutput: string; + public cmdInfo: string; + + private startTime: Map<string, number> = new Map<string, number>(); + + constructor( + private prjSvr: ProjectService, + private xdsSvr: XDSAgentService, + private fb: FormBuilder, + private alertSvr: AlertService, + private sdkSvr: SdkService, + private cookie: CookieService, + ) { + this.cmdOutput = ''; + this.cmdInfo = ''; // TODO: to be remove (only for debug) + this.buildForm = fb.group({ + subpath: this.subpathCtrl, + cmdClean: ['', Validators.nullValidator], + cmdPrebuild: ['', Validators.nullValidator], + cmdBuild: ['', Validators.nullValidator], + cmdPopulate: ['', Validators.nullValidator], + cmdArgs: ['', Validators.nullValidator], + envVars: ['', Validators.nullValidator], + }); + } + + ngOnInit() { + // Set default settings + // TODO save & restore values from cookies + this.buildForm.patchValue({ + subpath: '', + cmdClean: 'rm -rf build', + cmdPrebuild: 'mkdir -p build && cd build && cmake ..', + cmdBuild: 'cd build && make', + cmdPopulate: 'cd build && make remote-target-populate', + cmdArgs: '', + envVars: '', + }); + + // Command output data tunneling + this.xdsSvr.CmdOutput$.subscribe(data => { + this.cmdOutput += data.stdout; + this.cmdOutput += data.stderr; + }); + + // Command exit + this.xdsSvr.CmdExit$.subscribe(exit => { + if (this.startTime.has(exit.cmdID)) { + this.cmdInfo = 'Last command duration: ' + this._computeTime(this.startTime.get(exit.cmdID)); + this.startTime.delete(exit.cmdID); + } + + if (exit && exit.code !== 0) { + this.cmdOutput += '--- Command exited with code ' + exit.code + ' ---\n\n'; + } + }); + + this._scrollToBottom(); + + // only use for debug + this.debugEnable = (this.cookie.get('debug_build') === '1'); + } + + ngAfterViewChecked() { + this._scrollToBottom(); + } + + reset() { + this.cmdOutput = ''; + } + + clean() { + this._exec( + this.buildForm.value.cmdClean, + this.buildForm.value.subpath, + [], + this.buildForm.value.envVars); + } + + preBuild() { + this._exec( + this.buildForm.value.cmdPrebuild, + this.buildForm.value.subpath, + [], + this.buildForm.value.envVars); + } + + build() { + this._exec( + this.buildForm.value.cmdBuild, + this.buildForm.value.subpath, + [], + this.buildForm.value.envVars + ); + } + + populate() { + this._exec( + this.buildForm.value.cmdPopulate, + this.buildForm.value.subpath, + [], // args + this.buildForm.value.envVars + ); + } + + execCmd() { + this._exec( + this.buildForm.value.cmdArgs, + this.buildForm.value.subpath, + [], + this.buildForm.value.envVars + ); + } + + private _exec(cmd: string, dir: string, args: string[], env: string) { + if (!this.curProject) { + this.alertSvr.warning('No active project', true); + } + + // const prjID = this.curProject.id; + const prjID = this.prjSvr.getCurrent().id; + + this.cmdOutput += this._outputHeader(); + + const sdkid = this.sdkSvr.getCurrentId(); + + // Detect key=value in env string to build array of string + const envArr = []; + env.split(';').forEach(v => envArr.push(v.trim())); + + const t0 = performance.now(); + this.cmdInfo = 'Start build of ' + prjID + ' at ' + t0; + + this.xdsSvr.exec(prjID, dir, cmd, sdkid, args, envArr) + .subscribe(res => { + this.startTime.set(String(res.cmdID), t0); + }, + err => { + this.cmdInfo = 'Last command duration: ' + this._computeTime(t0); + this.alertSvr.error('ERROR: ' + err); + }); + } + + private _scrollToBottom(): void { + try { + this.scrollContainer.nativeElement.scrollTop = this.scrollContainer.nativeElement.scrollHeight; + } catch (err) { } + } + + private _computeTime(t0: number, t1?: number): string { + const enlap = Math.round((t1 || performance.now()) - t0); + if (enlap < 1000.0) { + return enlap.toFixed(2) + ' ms'; + } else { + return (enlap / 1000.0).toFixed(3) + ' seconds'; + } + } + + private _outputHeader(): string { + return '--- ' + new Date().toString() + ' ---\n'; + } + + private _outputFooter(): string { + return '\n'; + } +} diff --git a/webapp/src/app/pages/build/build.module.ts b/webapp/src/app/pages/build/build.module.ts new file mode 100644 index 0000000..ac1dfab --- /dev/null +++ b/webapp/src/app/pages/build/build.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { ThemeModule } from '../../@theme/theme.module'; + +import { BuildComponent } from './build.component'; +import { ProjectSelectDropdownComponent } from './settings/project-select-dropdown.component'; +import { SdkSelectDropdownComponent } from './settings/sdk-select-dropdown.component'; + +@NgModule({ + imports: [ + ThemeModule, + ], + declarations: [ + BuildComponent, + ProjectSelectDropdownComponent, + SdkSelectDropdownComponent, + ], + entryComponents: [ + ], +}) +export class BuildModule { } diff --git a/webapp/src/app/pages/build/settings/project-select-dropdown.component.ts b/webapp/src/app/pages/build/settings/project-select-dropdown.component.ts new file mode 100644 index 0000000..da3580a --- /dev/null +++ b/webapp/src/app/pages/build/settings/project-select-dropdown.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit, Input } from '@angular/core'; + +import { IProject, ProjectService } from '../../../@core-xds/services/project.service'; + +@Component({ + selector: 'xds-project-select-dropdown', + template: ` + <div class="form-group"> + <label>Project</label> + <select class="form-control"> + <option *ngFor="let prj of projects" (click)="select(prj)">{{prj.label}}</option> + </select> + </div> + `, +}) +export class ProjectSelectDropdownComponent implements OnInit { + + projects: IProject[]; + curPrj: IProject; + + constructor(private prjSvr: ProjectService) { } + + ngOnInit() { + this.curPrj = this.prjSvr.getCurrent(); + this.prjSvr.Projects$.subscribe((s) => { + if (s) { + this.projects = s; + if (this.curPrj === null || s.indexOf(this.curPrj) === -1) { + this.prjSvr.setCurrent(this.curPrj = s.length ? s[0] : null); + } + } + }); + } + + select(s) { + this.prjSvr.setCurrent(this.curPrj = s); + } +} + + diff --git a/webapp/src/app/sdks/sdkSelectDropdown.component.ts b/webapp/src/app/pages/build/settings/sdk-select-dropdown.component.ts index 7cd2dc7..562386d 100644 --- a/webapp/src/app/sdks/sdkSelectDropdown.component.ts +++ b/webapp/src/app/pages/build/settings/sdk-select-dropdown.component.ts @@ -1,21 +1,17 @@ import { Component, OnInit, Input } from '@angular/core'; -import { ISdk, SdkService } from '../services/sdk.service'; +import { ISdk, SdkService } from '../../../@core-xds/services/sdk.service'; @Component({ selector: 'xds-sdk-select-dropdown', template: ` - <div class="btn-group" dropdown *ngIf="curSdk" > - <button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;"> - {{curSdk.name}} <span class="caret" style="float: right; margin-top: 8px;"></span> - </button> - <ul *dropdownMenu class="dropdown-menu" role="menu"> - <li role="menuitem"><a class="dropdown-item" *ngFor="let sdk of sdks" (click)="select(sdk)"> - {{sdk.name}}</a> - </li> - </ul> - </div> - ` + <div class="form-group"> + <label>SDK</label> + <select class="form-control"> + <option *ngFor="let sdk of sdks" (click)="select(sdk)">{{sdk.name}}</option> + </select> + </div> + `, }) export class SdkSelectDropdownComponent implements OnInit { diff --git a/webapp/src/app/pages/config/config-global/config-global.component.html b/webapp/src/app/pages/config/config-global/config-global.component.html new file mode 100644 index 0000000..0038510 --- /dev/null +++ b/webapp/src/app/pages/config/config-global/config-global.component.html @@ -0,0 +1,37 @@ +<div class="row"> + <div class="col-md-12"> + <nb-card> + <nb-card-header>Global Configuration</nb-card-header> + <nb-card-body> + + <form (ngSubmit)="onSubmit()" #ConfigGlobalForm="ngForm"> + <div class="form-group row"> + <label class="col-sm-3 col-form-label">Language</label> + <div class="col-sm-9"> + <select class="form-control" (ngModelChange)="configFormChanged=true"> + <option>English</option> + <!-- FIXME: implement i18n and add | translate + <option>French</option> --> + </select> + </div> + </div> + + <div class="form-group row"> + <label class="col-sm-3 col-form-label">Theme</label> + <div class="col-sm-9"> + <ngx-theme-switcher id="theme-switcher"></ngx-theme-switcher> + </div> + </div> + + <!-- + <div class="form-group row"> + <div class="offset-sm-3 col-sm-9"> + <button type="submit" class="btn btn-primary" [disabled]="!configFormChanged">Apply</button> + </div> + </div> + --> + </form> + </nb-card-body> + </nb-card> + </div> +</div> diff --git a/webapp/src/app/pages/config/config-global/config-global.component.scss b/webapp/src/app/pages/config/config-global/config-global.component.scss new file mode 100644 index 0000000..955c54e --- /dev/null +++ b/webapp/src/app/pages/config/config-global/config-global.component.scss @@ -0,0 +1,20 @@ +.full-width { + flex: 1; + min-width: 220px; +} + +nb-checkbox { + margin-bottom: 1rem; +} + +.form-inline > * { + margin: 0 1.5rem 1.5rem 0; +} + +nb-card.inline-form-card nb-card-body { + padding-bottom: 0; +} + +#theme-switcher { + align-items: left; +} diff --git a/webapp/src/app/pages/config/config-global/config-global.component.ts b/webapp/src/app/pages/config/config-global/config-global.component.ts new file mode 100644 index 0000000..fcd8b62 --- /dev/null +++ b/webapp/src/app/pages/config/config-global/config-global.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; + +import { ConfigService, IConfig } from '../../../@core-xds/services/config.service'; + +@Component({ + selector: 'xds-config-global', + styleUrls: ['./config-global.component.scss'], + templateUrl: './config-global.component.html', +}) +export class ConfigGlobalComponent implements OnInit { + + public configFormChanged = false; + + constructor( + private configSvr: ConfigService, + ) { + } + + ngOnInit() { + } + + onSubmit() { + } +} + diff --git a/webapp/src/app/pages/config/config-routing.module.ts b/webapp/src/app/pages/config/config-routing.module.ts new file mode 100644 index 0000000..4e7cf27 --- /dev/null +++ b/webapp/src/app/pages/config/config-routing.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { ConfigComponent } from './config.component'; +import { ConfigGlobalComponent } from './config-global/config-global.component'; +import { ConfigXdsComponent } from './config-xds/config-xds.component'; + +const routes: Routes = [{ + path: '', + component: ConfigComponent, + children: [ + { + path: 'global', + component: ConfigGlobalComponent, + }, { + path: 'xds', + component: ConfigXdsComponent, + }, + ], +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class ConfigRoutingModule { } + +export const routedConfig = [ + ConfigComponent, + ConfigGlobalComponent, + ConfigXdsComponent, +]; diff --git a/webapp/src/app/pages/config/config-xds/config-xds.component.html b/webapp/src/app/pages/config/config-xds/config-xds.component.html new file mode 100644 index 0000000..31559e2 --- /dev/null +++ b/webapp/src/app/pages/config/config-xds/config-xds.component.html @@ -0,0 +1,34 @@ +<div class="row"> + <div class="col-md-12"> + <nb-card> + <nb-card-header>XDS Server Configuration</nb-card-header> + <nb-card-body> + <form (ngSubmit)="onSubmit()" #ConfigXdsForm="ngForm"> + <div class="form-group row"> + <label class="col-sm-3 col-form-label">XDS Server URL</label> + <div class="col-sm-8"> + <input type="url" class="form-control" [ngClass]="{ 'form-control-danger': !server.connected }" id="inputServerUrl" [(ngModel)]="xdsServerUrl" name="serverUrl" (ngModelChange)="configFormChanged=true" [disabled]="connecting"> + </div> + <div class="col-sm-1"> + <span *ngIf="!connecting" class="fa fa-fw fa-exchange fa-size-x2 vcenter" [style.color]="server.connected?'green':'red'"></span> + <span *ngIf="connecting" class="fa fa-gear faa-spin animated fa-size-x2 vcenter"></span> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-3 col-form-label">XDS Server connection retry</label> + <div class="col-sm-8"> + <input type="number" class="form-control" id="inputServerConnRetry" [(ngModel)]="server.connRetry" name="serverRetry" (ngModelChange)="configFormChanged=true"> + </div> + </div> + + <div class="form-group row"> + <div class="offset-sm-3 col-sm-9"> + <button type="submit" class="btn btn-primary" [disabled]=" + connecting || (server.connected && !configFormChanged)">Apply</button> + </div> + </div> + </form> + </nb-card-body> + </nb-card> + </div> +</div> diff --git a/webapp/src/app/pages/config/config-xds/config-xds.component.scss b/webapp/src/app/pages/config/config-xds/config-xds.component.scss new file mode 100644 index 0000000..d7571b9 --- /dev/null +++ b/webapp/src/app/pages/config/config-xds/config-xds.component.scss @@ -0,0 +1,26 @@ +.full-width { + flex: 1; + min-width: 220px; +} + +nb-checkbox { + margin-bottom: 1rem; +} + +.form-inline > * { + margin: 0 1.5rem 1.5rem 0; +} + +nb-card.inline-form-card nb-card-body { + padding-bottom: 0; +} + +.fa-size-x2 { + font-size: 20px; +} + +.vcenter { + //display: inline-block; + //vertical-align: middle; + margin-top: 33%; +} diff --git a/webapp/src/app/pages/config/config-xds/config-xds.component.ts b/webapp/src/app/pages/config/config-xds/config-xds.component.ts new file mode 100644 index 0000000..ffd236d --- /dev/null +++ b/webapp/src/app/pages/config/config-xds/config-xds.component.ts @@ -0,0 +1,63 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { XDSConfigService } from '../../../@core-xds/services/xds-config.service'; +import { IXDServerCfg } from '../../../@core-xds/services/xdsagent.service'; +import { AlertService, IAlert } from '../../../@core-xds/services/alert.service'; +import { NotificationsComponent } from '../../notifications/notifications.component'; + +// Import RxJs required methods +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/catch'; + +@Component({ + selector: 'xds-config-xds', + styleUrls: ['./config-xds.component.scss'], + templateUrl: './config-xds.component.html', +}) +export class ConfigXdsComponent implements OnInit { + + // TODO: cleanup agentStatus$: Observable<IAgentStatus>; + connecting = false; + xdsServerUrl = ''; + server: IXDServerCfg; + + configFormChanged = false; + + constructor( + private XdsConfigSvr: XDSConfigService, + private alert: AlertService, + ) { + } + + ngOnInit() { + // FIXME support multiple servers + + this.server = this.XdsConfigSvr.getCurServer(); + this.xdsServerUrl = this.server.url; + + this.XdsConfigSvr.onCurServer().subscribe(svr => { + this.xdsServerUrl = svr.url; + this.server = svr; + }); + } + + onSubmit() { + if (!this.configFormChanged && this.server.connected) { + return; + } + this.configFormChanged = false; + this.connecting = true; + this.server.url = this.xdsServerUrl; + this.XdsConfigSvr.setCurServer(this.server) + .subscribe(cfg => { + this.connecting = false; + }, + err => { + this.connecting = false; + this.alert.error(err); + }); + } + +} + diff --git a/webapp/src/app/config/downloadXdsAgent.component.ts b/webapp/src/app/pages/config/config-xds/downloadXdsAgent.component.ts index 3901331..3901331 100644 --- a/webapp/src/app/config/downloadXdsAgent.component.ts +++ b/webapp/src/app/pages/config/config-xds/downloadXdsAgent.component.ts diff --git a/webapp/src/app/pages/config/config.component.ts b/webapp/src/app/pages/config/config.component.ts new file mode 100644 index 0000000..f52cab0 --- /dev/null +++ b/webapp/src/app/pages/config/config.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'xds-config', + template: ` + <router-outlet></router-outlet> + `, +}) +export class ConfigComponent { +} diff --git a/webapp/src/app/pages/config/config.module.ts b/webapp/src/app/pages/config/config.module.ts new file mode 100644 index 0000000..2fdaf94 --- /dev/null +++ b/webapp/src/app/pages/config/config.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; + +import { ThemeModule } from '../../@theme/theme.module'; +import { ConfigRoutingModule, routedConfig } from './config-routing.module'; + +@NgModule({ + imports: [ + ThemeModule, + ConfigRoutingModule, + ], + declarations: [ + ...routedConfig, + ] +}) +export class ConfigModule { } diff --git a/webapp/src/app/pages/dashboard/dashboard.component.html b/webapp/src/app/pages/dashboard/dashboard.component.html new file mode 100644 index 0000000..9160019 --- /dev/null +++ b/webapp/src/app/pages/dashboard/dashboard.component.html @@ -0,0 +1,10 @@ +<div class="row"> + Dashboard page... + <!-- + <div class="col-xxxl-3 col-md-6"> + <ngx-status-card title="Light" type="primary"> + <i class="nb-lightbulb"></i> + </ngx-status-card> + </div> +--> +</div> diff --git a/webapp/src/app/pages/dashboard/dashboard.component.scss b/webapp/src/app/pages/dashboard/dashboard.component.scss new file mode 100644 index 0000000..6f1f0e0 --- /dev/null +++ b/webapp/src/app/pages/dashboard/dashboard.component.scss @@ -0,0 +1,16 @@ +@import '../../@theme/styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + .solar-card nb-card-header { + border: none; + padding-bottom: 0; + } + + @include media-breakpoint-down(is) { + /deep/ nb-card.large-card { + height: nb-theme(card-height-medium); + } + } +} diff --git a/webapp/src/app/pages/dashboard/dashboard.component.ts b/webapp/src/app/pages/dashboard/dashboard.component.ts new file mode 100644 index 0000000..a4539cb --- /dev/null +++ b/webapp/src/app/pages/dashboard/dashboard.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngx-dashboard', + styleUrls: ['./dashboard.component.scss'], + templateUrl: './dashboard.component.html', +}) +export class DashboardComponent { +} diff --git a/webapp/src/app/pages/dashboard/dashboard.module.ts b/webapp/src/app/pages/dashboard/dashboard.module.ts new file mode 100644 index 0000000..725df7a --- /dev/null +++ b/webapp/src/app/pages/dashboard/dashboard.module.ts @@ -0,0 +1,52 @@ +import { NgModule } from '@angular/core'; +import { AngularEchartsModule } from 'ngx-echarts'; + +import { ThemeModule } from '../../@theme/theme.module'; +import { DashboardComponent } from './dashboard.component'; +import { StatusCardComponent } from './status-card/status-card.component'; +/* +import { ContactsComponent } from './contacts/contacts.component'; +import { RoomsComponent } from './rooms/rooms.component'; +import { RoomSelectorComponent } from './rooms/room-selector/room-selector.component'; +import { TemperatureComponent } from './temperature/temperature.component'; +import { TemperatureDraggerComponent } from './temperature/temperature-dragger/temperature-dragger.component'; +import { TeamComponent } from './team/team.component'; +import { KittenComponent } from './kitten/kitten.component'; +import { SecurityCamerasComponent } from './security-cameras/security-cameras.component'; +import { ElectricityComponent } from './electricity/electricity.component'; +import { ElectricityChartComponent } from './electricity/electricity-chart/electricity-chart.component'; +import { WeatherComponent } from './weather/weather.component'; +import { SolarComponent } from './solar/solar.component'; +import { PlayerComponent } from './rooms/player/player.component'; +import { TrafficComponent } from './traffic/traffic.component'; +import { TrafficChartComponent } from './traffic/traffic-chart.component'; +*/ + +@NgModule({ + imports: [ + ThemeModule, + AngularEchartsModule, + ], + declarations: [ + DashboardComponent, + StatusCardComponent, + /* + TemperatureDraggerComponent, + ContactsComponent, + RoomSelectorComponent, + TemperatureComponent, + RoomsComponent, + TeamComponent, + KittenComponent, + SecurityCamerasComponent, + ElectricityComponent, + ElectricityChartComponent, + WeatherComponent, + PlayerComponent, + SolarComponent, + TrafficComponent, + TrafficChartComponent, + */ + ], +}) +export class DashboardModule { } diff --git a/webapp/src/app/pages/dashboard/status-card/status-card.component.scss b/webapp/src/app/pages/dashboard/status-card/status-card.component.scss new file mode 100644 index 0000000..08abc61 --- /dev/null +++ b/webapp/src/app/pages/dashboard/status-card/status-card.component.scss @@ -0,0 +1,142 @@ +@import '../../../@theme/styles/themes'; +@import '~@nebular/theme/styles/global/bootstrap/hero-buttons'; + +@include nb-install-component() { + nb-card { + flex-direction: row; + align-items: center; + height: 6rem; + overflow: visible; + + $bevel: btn-hero-bevel(nb-theme(card-bg)); + $shadow: nb-theme(btn-hero-shadow); + box-shadow: $bevel, $shadow; + + .icon-container { + height: 100%; + padding: 0.625rem; + } + + .icon { + display: flex; + align-items: center; + justify-content: center; + width: 5.75rem; + height: 4.75rem; + font-size: 3.75rem; + border-radius: nb-theme(card-border-radius); + transition: width 0.4s ease; + transform: translate3d(0, 0, 0); + -webkit-transform-style: preserve-3d; + -webkit-backface-visibility: hidden; + color: nb-theme(color-white); + + &.primary { + @include btn-hero-primary-gradient(); + @include btn-hero-primary-bevel-glow-shadow(); + } + &.success { + @include btn-hero-success-gradient(); + @include btn-hero-success-bevel-glow-shadow(); + } + &.info { + @include btn-hero-info-gradient(); + @include btn-hero-info-bevel-glow-shadow(); + } + &.warning { + @include btn-hero-warning-gradient(); + @include btn-hero-warning-bevel-glow-shadow(); + } + } + + &:hover { + background: lighten(nb-theme(card-bg), 5%); + + .icon { + &.primary { + background-image: btn-hero-primary-light-gradient(); + } + &.success { + background-image: btn-hero-success-light-gradient(); + } + &.info { + background-image: btn-hero-info-light-gradient(); + } + &.warning { + background-image: btn-hero-warning-light-gradient(); + } + } + } + + &.off { + color: nb-theme(card-fg); + + .icon { + color: nb-theme(card-fg); + + &.primary, &.success, &.info, &.warning { + box-shadow: none; + background-image: linear-gradient(to right, transparent, transparent); + } + } + + .title { + color: nb-theme(card-fg); + } + } + + .details { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + padding: 0 0.5rem 0 0.75rem; + border-left: 1px solid transparent; + } + + .title { + font-family: nb-theme(font-secondary); + font-size: 1.25rem; + font-weight: nb-theme(font-weight-bold); + color: nb-theme(card-fg-heading); + } + + .status { + font-size: 1rem; + font-weight: nb-theme(font-weight-light); + text-transform: uppercase; + color: nb-theme(card-fg); + } + } + + @include nb-for-theme(cosmic) { + nb-card { + &.off .icon-container { + border-right: 1px solid nb-theme(separator); + } + + .icon-container { + padding: 0; + } + + .details { + padding-left: 1.25rem; + } + + .icon { + width: 7rem; + height: 100%; + font-size: 4.5rem; + border-radius: nb-theme(card-border-radius) 0 0 nb-theme(card-border-radius); + } + + .title { + font-weight: nb-theme(font-weight-bolder); + } + + .status { + font-weight: nb-theme(font-weight-light); + } + } + } +} diff --git a/webapp/src/app/pages/dashboard/status-card/status-card.component.ts b/webapp/src/app/pages/dashboard/status-card/status-card.component.ts new file mode 100644 index 0000000..6260803 --- /dev/null +++ b/webapp/src/app/pages/dashboard/status-card/status-card.component.ts @@ -0,0 +1,26 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ngx-status-card', + styleUrls: ['./status-card.component.scss'], + template: ` + <nb-card (click)="on = !on" [ngClass]="{'off': !on}"> + <div class="icon-container"> + <div class="icon {{ type }}"> + <ng-content></ng-content> + </div> + </div> + + <div class="details"> + <div class="title">{{ title }}</div> + <div class="status">{{ on ? 'ON' : 'OFF' }}</div> + </div> + </nb-card> + `, +}) +export class StatusCardComponent { + + @Input() title: string; + @Input() type: string; + @Input() on = true; +} diff --git a/webapp/src/app/pages/notifications/notifications.component.scss b/webapp/src/app/pages/notifications/notifications.component.scss new file mode 100644 index 0000000..ce85e8e --- /dev/null +++ b/webapp/src/app/pages/notifications/notifications.component.scss @@ -0,0 +1,28 @@ +@import '../../@theme/styles/themes'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; + +@include nb-install-component() { + + nb-card-footer { + padding-bottom: 0.25rem; + + button { + margin: 0 1rem 1rem 0; + } + } + + /* stylelint-disable */ + toaster-container /deep/ { + #toast-container .toast-close-button { + right: 0; + } + } + /* stylelint-enable */ + + @include media-breakpoint-down(xs) { + .dropdown-toggle { + font-size: 0.75rem; + } + } +} diff --git a/webapp/src/app/pages/notifications/notifications.component.ts b/webapp/src/app/pages/notifications/notifications.component.ts new file mode 100644 index 0000000..accd150 --- /dev/null +++ b/webapp/src/app/pages/notifications/notifications.component.ts @@ -0,0 +1,65 @@ +import { Component } from '@angular/core'; +import { ToasterService, ToasterConfig, Toast, BodyOutputType } from 'angular2-toaster'; +import { Observable } from 'rxjs/Observable'; +import { AlertService, IAlert } from '../../@core-xds/services/alert.service'; + +import 'style-loader!angular2-toaster/toaster.css'; + +@Component({ + selector: 'ngx-notifications', + styleUrls: ['./notifications.component.scss'], + template: '<toaster-container [toasterconfig]="config"></toaster-container>', +}) +export class NotificationsComponent { + + config: ToasterConfig; + + private position = 'toast-top-full-width'; + private animationType = 'slideDown'; + private toastsLimit = 10; + private toasterService: ToasterService; + private alerts$: Observable<IAlert[]>; + + constructor( + toasterService: ToasterService, + private alertSvr: AlertService, + ) { + this.toasterService = toasterService; + + this.alertSvr.alerts.subscribe(alerts => { + if (alerts.length === 0) { + this.clearToasts(); + } else { + alerts.forEach(al => { + const title = al.type.toUpperCase(); + this.showToast(al.type, title, al.msg, al.dismissTimeout); + }); + } + }); + } + + private showToast(type: string, title: string, body: string, tmo: number) { + this.config = new ToasterConfig({ + positionClass: this.position, + timeout: tmo, + newestOnTop: true, + tapToDismiss: true, // is Hide OnClick + preventDuplicates: false, + animation: this.animationType, + limit: this.toastsLimit, + }); + const toast: Toast = { + type: type, + title: title, + body: body, + timeout: tmo, + showCloseButton: true, + bodyOutputType: BodyOutputType.TrustedHtml, + }; + this.toasterService.popAsync(toast); + } + + clearToasts() { + this.toasterService.clear(); + } +} diff --git a/webapp/src/app/pages/pages-menu.ts b/webapp/src/app/pages/pages-menu.ts new file mode 100644 index 0000000..1a3dd97 --- /dev/null +++ b/webapp/src/app/pages/pages-menu.ts @@ -0,0 +1,201 @@ +import { NbMenuItem } from '@nebular/theme'; + +export const MENU_ITEMS: NbMenuItem[] = [ + { + title: 'XDS-Dashboard', + icon: 'nb-home', + link: '/pages/dashboard', + home: true, + }, + { + title: 'DEVELOPMENT', + group: true, + }, + { + title: 'Projects', + icon: 'nb-keypad', + link: '/pages/projects', + }, + { + title: 'SDKs', + icon: 'fa fa-file-archive-o', + link: '/pages/sdks', + }, + { + title: 'Boards', + icon: 'fa fa-microchip', + children: [ + ], + }, + { + title: 'Build', + icon: 'fa fa-cogs', + link: '/pages/build', + }, + { + title: 'MISC', + group: true, + }, + + { + title: 'Configuration', + icon: 'fa fa-sliders', + link: '/pages/config', + children: [ + { + title: 'Global', + link: '/pages/config/global', + }, + { + title: 'XDS Server', + link: '/pages/config/xds', + }, + ], + }, + /* + { + title: 'UI Features', + icon: 'nb-keypad', + link: '/pages/ui-features', + children: [ + { + title: 'Buttons', + link: '/pages/ui-features/buttons', + }, + { + title: 'Grid', + link: '/pages/ui-features/grid', + }, + { + title: 'Icons', + link: '/pages/ui-features/icons', + }, + { + title: 'Modals', + link: '/pages/ui-features/modals', + }, + { + title: 'Typography', + link: '/pages/ui-features/typography', + }, + { + title: 'Animated Searches', + link: '/pages/ui-features/search-fields', + }, + { + title: 'Tabs', + link: '/pages/ui-features/tabs', + }, + ], + }, + { + title: 'Forms', + icon: 'nb-compose', + children: [ + { + title: 'Form Inputs', + link: '/pages/forms/inputs', + }, + { + title: 'Form Layouts', + link: '/pages/forms/layouts', + }, + ], + }, + { + title: 'Components', + icon: 'nb-gear', + children: [ + { + title: 'Tree', + link: '/pages/components/tree', + }, { + title: 'Notifications', + link: '/pages/components/notifications', + }, + ], + }, + { + title: 'Maps', + icon: 'nb-location', + children: [ + { + title: 'Google Maps', + link: '/pages/maps/gmaps', + }, + { + title: 'Leaflet Maps', + link: '/pages/maps/leaflet', + }, + { + title: 'Bubble Maps', + link: '/pages/maps/bubble', + }, + ], + }, + { + title: 'Charts', + icon: 'nb-bar-chart', + children: [ + { + title: 'Echarts', + link: '/pages/charts/echarts', + }, + { + title: 'Charts.js', + link: '/pages/charts/chartjs', + }, + { + title: 'D3', + link: '/pages/charts/d3', + }, + ], + }, + { + title: 'Editors', + icon: 'nb-title', + children: [ + { + title: 'TinyMCE', + link: '/pages/editors/tinymce', + }, + { + title: 'CKEditor', + link: '/pages/editors/ckeditor', + }, + ], + }, + { + title: 'Tables', + icon: 'nb-tables', + children: [ + { + title: 'Smart Table', + link: '/pages/tables/smart-table', + }, + ], + }, + */ + { + title: 'Auth', + icon: 'nb-locked', + children: [ + { + title: 'Login', + link: '/auth/login', + }, + { + title: 'Register', + link: '/auth/register', + }, + { + title: 'Request Password', + link: '/auth/request-password', + }, + { + title: 'Reset Password', + link: '/auth/reset-password', + }, + ], + }, +]; diff --git a/webapp/src/app/pages/pages-routing.module.ts b/webapp/src/app/pages/pages-routing.module.ts new file mode 100644 index 0000000..11834e8 --- /dev/null +++ b/webapp/src/app/pages/pages-routing.module.ts @@ -0,0 +1,41 @@ +import { RouterModule, Routes } from '@angular/router'; +import { NgModule } from '@angular/core'; + +import { PagesComponent } from './pages.component'; +import { DashboardComponent } from './dashboard/dashboard.component'; +import { ProjectsComponent } from './projects/projects.component'; +import { SdksComponent } from './sdks/sdks.component'; +import { BuildComponent } from './build/build.component'; + +const routes: Routes = [{ + path: '', + component: PagesComponent, + children: [{ + path: 'dashboard', + component: DashboardComponent, + }, { + path: 'projects', + component: ProjectsComponent, + }, { + path: 'sdks', + component: SdksComponent, + }, { + path: 'build', + component: BuildComponent, + }, { + path: 'config', + loadChildren: './config/config.module#ConfigModule', + }, + { + path: '', + redirectTo: 'dashboard', + pathMatch: 'full', + }], +}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class PagesRoutingModule { +} diff --git a/webapp/src/app/pages/pages.component.ts b/webapp/src/app/pages/pages.component.ts new file mode 100644 index 0000000..a343565 --- /dev/null +++ b/webapp/src/app/pages/pages.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; + +import { MENU_ITEMS } from './pages-menu'; + +@Component({ + selector: 'ngx-pages', + template: ` + <ngx-notifications></ngx-notifications> + <ngx-xds-layout> + <nb-menu [items]="menu"></nb-menu> + <router-outlet></router-outlet> + </ngx-xds-layout> + `, +}) +export class PagesComponent { + + menu = MENU_ITEMS; +} diff --git a/webapp/src/app/pages/pages.module.ts b/webapp/src/app/pages/pages.module.ts new file mode 100644 index 0000000..5e4f3be --- /dev/null +++ b/webapp/src/app/pages/pages.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { ToasterModule } from 'angular2-toaster'; + +import { PagesComponent } from './pages.component'; +import { DashboardModule } from './dashboard/dashboard.module'; +import { BuildModule } from './build/build.module'; +import { ProjectsModule } from './projects/projects.module'; +import { SdksModule } from './sdks/sdks.module'; +import { PagesRoutingModule } from './pages-routing.module'; +import { NotificationsComponent } from './notifications/notifications.component'; +import { ThemeModule } from '../@theme/theme.module'; + +const PAGES_COMPONENTS = [ + PagesComponent, + NotificationsComponent, +]; + +@NgModule({ + imports: [ + PagesRoutingModule, + ThemeModule, + BuildModule, + DashboardModule, + ProjectsModule, + SdksModule, + ToasterModule, + ], + declarations: [ + ...PAGES_COMPONENTS, + ], +}) +export class PagesModule { +} diff --git a/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.html b/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.html new file mode 100644 index 0000000..e2c6748 --- /dev/null +++ b/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.html @@ -0,0 +1,51 @@ +<div class="modal-header"> + <span>Add a new project</span> + <button class="close" aria-label="Close" (click)="closeModal()"> + <span aria-hidden="true">×</span> + </button> +</div> + +<div class="modal-body row"> + <div class="col-12"> + <form [formGroup]="addProjectForm" (ngSubmit)="onSubmit()"> + + <div class="form-group row"> + <label for="sharing-type" class="col-sm-3 col-form-label">Sharing Type</label> + <div class="col-sm-9"> + <select id="select-sharing-type" class="form-control" formControlName="type"> + <option *ngFor="let t of projectTypes" [value]="t.value">{{t.display}} + </option> + </select> + </div> + </div> + + <div class="form-group row"> + <label for="select-local-path" class="col-sm-3 col-form-label">Local Path</label> + <div class="col-sm-9"> + <input type="text" id="inputLocalPath" class="form-control" formControlName="pathCli" placeholder="/tmp/myProject" (change)="onChangeLocalProject($event)"> + </div> + </div> + + <div class="form-group row"> + <label for="select-server-path" class="col-sm-3 col-form-label">Server Path</label> + <div class="col-sm-9"> + <input type="text" id="inputServerPath" class="form-control" formControlName="pathSvr"> + </div> + </div> + + <div class="form-group row"> + <label for="select-label" class="col-sm-3 col-form-label">Label</label> + <div class="col-sm-9"> + <input type="text" id="inputLabel" class="form-control" formControlName="label" (keyup)="onKeyLabel($event)"> + </div> + </div> + + <div class="offset-sm-3 col-sm-9"> + <button class="btn btn-md btn-secondary" (click)="cancelAction=true; closeModal()"> Cancel </button> + <button class="btn btn-md btn-primary" type="submit" [disabled]="!addProjectForm.valid">Add Folder</button> + </div> + </form> + </div> +</div> +<div class="modal-footer form-group"> +</div> diff --git a/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.scss b/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.scss @@ -0,0 +1 @@ + diff --git a/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.ts b/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.ts new file mode 100644 index 0000000..640ac5c --- /dev/null +++ b/webapp/src/app/pages/projects/project-add-modal/project-add-modal.component.ts @@ -0,0 +1,143 @@ +import { Component, Input, ViewChild, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { FormControl, FormGroup, Validators, ValidationErrors, FormBuilder, ValidatorFn, AbstractControl } from '@angular/forms'; + +// Import RxJs required methods +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/debounceTime'; + +import { AlertService, IAlert } from '../../../@core-xds/services/alert.service'; +import { ProjectService, IProject, ProjectType, ProjectTypes } from '../../../@core-xds/services/project.service'; +import { XDSConfigService } from '../../../@core-xds/services/xds-config.service'; + + +@Component({ + selector: 'xds-project-add-modal', + templateUrl: 'project-add-modal.component.html', + styleUrls: ['project-add-modal.component.scss'] +}) +export class ProjectAddModalComponent implements OnInit { + // @Input('server-id') serverID: string; + private serverID: string; + + cancelAction = false; + userEditedLabel = false; + projectTypes = ProjectTypes; + + addProjectForm: FormGroup; + typeCtrl: FormControl; + pathCliCtrl: FormControl; + pathSvrCtrl: FormControl; + + constructor( + private alert: AlertService, + private projectSvr: ProjectService, + private XdsConfigSvr: XDSConfigService, + private fb: FormBuilder, + private activeModal: NgbActiveModal + ) { + // Define types (first one is special/placeholder) + this.projectTypes.unshift({ value: ProjectType.UNSET, display: '--Select a type--' }); + + this.typeCtrl = new FormControl(this.projectTypes[0].value, this.validatorProjType); + this.pathCliCtrl = new FormControl('', this.validatorProjPath); + this.pathSvrCtrl = new FormControl({ value: '', disabled: true }, this.validatorProjPath); + + this.addProjectForm = fb.group({ + type: this.typeCtrl, + pathCli: this.pathCliCtrl, + pathSvr: this.pathSvrCtrl, + label: ['', Validators.nullValidator], + }); + } + + + ngOnInit() { + // Update server ID + this.serverID = this.XdsConfigSvr.getCurServer().id; + this.XdsConfigSvr.onCurServer().subscribe(svr => this.serverID = svr.id); + + // Auto create label name + this.pathCliCtrl.valueChanges + .debounceTime(100) + .filter(n => n) + .map(n => { + const last = n.split('/'); + let nm = n; + if (last.length > 0) { + nm = last.pop(); + if (nm === '' && last.length > 0) { + nm = last.pop(); + } + } + return 'Project_' + nm; + }) + .subscribe(value => { + if (value && !this.userEditedLabel) { + this.addProjectForm.patchValue({ label: value }); + } + }); + + // Handle disabling of Server path + this.typeCtrl.valueChanges + .debounceTime(500) + .subscribe(valType => { + const dis = (valType === String(ProjectType.SYNCTHING)); + this.pathSvrCtrl.reset({ value: '', disabled: dis }); + }); + } + + closeModal() { + this.activeModal.close(); + } + + onKeyLabel(event: any) { + this.userEditedLabel = (this.addProjectForm.value.label !== ''); + } + + onChangeLocalProject(e) { + } + + onSubmit() { + if (this.cancelAction) { + return; + } + + const formVal = this.addProjectForm.value; + + const type = formVal['type'].value; + this.projectSvr.Add({ + serverId: this.serverID, + label: formVal['label'], + pathClient: formVal['pathCli'], + pathServer: formVal['pathSvr'], + type: formVal['type'], + // FIXME: allow to set defaultSdkID from New Project config panel + }) + .subscribe(prj => { + this.alert.info('Project ' + prj.label + ' successfully created.'); + this.closeModal(); + + // Reset Value for the next creation + this.addProjectForm.reset(); + const selectedType = this.projectTypes[0].value; + this.addProjectForm.patchValue({ type: selectedType }); + + }, + err => { + this.alert.error(err, 60); + this.closeModal(); + }); + } + + private validatorProjType(g: FormGroup): ValidationErrors | null { + return (g.value !== ProjectType.UNSET) ? null : { validatorProjType: { valid: false } }; + } + + private validatorProjPath(g: FormGroup): ValidationErrors | null { + return (g.disabled || g.value !== '') ? null : { validatorProjPath: { valid: false } }; + } + +} diff --git a/webapp/src/app/pages/projects/project-card/project-card.component.html b/webapp/src/app/pages/projects/project-card/project-card.component.html new file mode 100644 index 0000000..c54d581 --- /dev/null +++ b/webapp/src/app/pages/projects/project-card/project-card.component.html @@ -0,0 +1,65 @@ +<nb-card class="xds-projects"> + <nb-card-header> + + <div class="row"> + <div class="col-12 col-md-8"> + {{ project.label }} + </div> + <div class="col-6 col-md-4 text-right" role="group"> + <button class="btn btn-outline-danger btn-tn btn-xds" (click)="delete(project)"> + <span class="fa fa-trash fa-size-x2"></span> + </button> + </div> + </div> + </nb-card-header> + + <nb-card-body> + <table class="table table-striped"> + <tbody> + <tr> + <th> + <span class="fa fa-fw fa-id-badge"></span> + <span>Project ID</span> + </th> + <td>{{ project.id }}</td> + </tr> + <tr> + <th> + <span class="fa fa-fw fa-exchange"></span> + <span>Sharing type</span> + </th> + <td>{{ project.type | readableType }}</td> + </tr> + <tr> + <th> + <span class="fa fa-fw fa-folder-open-o"></span> + <span>Local path</span> + </th> + <td>{{ project.pathClient }}</td> + </tr> + <tr *ngIf="project.pathServer && project.pathServer != ''"> + <th> + <span class="fa fa-fw fa-folder-open-o"></span> + <span>Server path</span> + </th> + <td>{{ project.pathServer }}</td> + </tr> + <tr> + <th> + <span class="fa fa-fw fa-flag"></span> + <span>Status</span> + </th> + <td>{{ project.status }} - {{ project.isInSync ? "Up to Date" : "Out of Sync"}} + <button *ngIf="!project.isInSync" class="btn btn-outline-info btn-tn btn-xds" (click)="sync(project)" style="margin-left:2em;"> + <span class="fa fa-refresh fa-size-x2"></span> + </button> + </td> + </tr> + </tbody> + </table> + </nb-card-body> + + <nb-card-footer> + <!-- <pre>{{project | json}}</pre> --> + </nb-card-footer> +</nb-card> diff --git a/webapp/src/app/pages/projects/project-card/project-card.component.scss b/webapp/src/app/pages/projects/project-card/project-card.component.scss new file mode 100644 index 0000000..a433f58 --- /dev/null +++ b/webapp/src/app/pages/projects/project-card/project-card.component.scss @@ -0,0 +1,54 @@ +@import '~@nebular/theme/styles/global/bootstrap/buttons'; + +.xds-project-card .icon { + padding: 0.75rem 0; + font-size: 1.75rem; +} + +nb-card-body { + padding: 0; +} + +nb-card-footer { + text-align: right; +} + +.fa-big { + font-size: 20px; + font-weight: bold; +} + +.fa-size-x2 { + font-size: 20px; +} + +th span { + font-weight: 100; +} + +th label { + font-weight: 100; + margin-bottom: 0; +} + +tr.info>th { + vertical-align: middle; +} + +tr.info>td { + vertical-align: middle; +} + +.btn-outline-danger.btn-xds { + color: #ff4c6a; + &:focus { + color: white; + } +} + +.btn-outline-info.btn-xds { + color: #4ca6ff; + &:focus { + color: white; + } +} diff --git a/webapp/src/app/pages/projects/project-card/project-card.component.ts b/webapp/src/app/pages/projects/project-card/project-card.component.ts new file mode 100644 index 0000000..160c4c8 --- /dev/null +++ b/webapp/src/app/pages/projects/project-card/project-card.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, Pipe, PipeTransform } from '@angular/core'; +import { ProjectService, IProject, ProjectType, ProjectTypeEnum } from '../../../@core-xds/services/project.service'; +import { AlertService } from '../../../@core-xds/services/alert.service'; + + +@Component({ + selector: 'xds-project-card', + styleUrls: ['./project-card.component.scss'], + templateUrl: './project-card.component.html', +}) +export class ProjectCardComponent { + + // FIXME workaround of https://github.com/angular/angular-cli/issues/2034 + // should be removed with angular 5 + // @Input() project: IProject; + @Input() project = <IProject>null; + + constructor( + private alert: AlertService, + private projectSvr: ProjectService + ) { + } + + delete(prj: IProject) { + this.projectSvr.Delete(prj).subscribe( + res => { }, + err => this.alert.error('ERROR delete: ' + err) + ); + } + + sync(prj: IProject) { + this.projectSvr.Sync(prj).subscribe( + res => { }, + err => this.alert.error('ERROR: ' + err) + ); + } +} + +// Make Project type human readable +@Pipe({ + name: 'readableType' +}) + +export class ProjectReadableTypePipe implements PipeTransform { + transform(type: ProjectTypeEnum): string { + switch (type) { + case ProjectType.NATIVE_PATHMAP: return 'Native (path mapping)'; + case ProjectType.SYNCTHING: return 'Cloud (Syncthing)'; + default: return String(type); + } + } +} diff --git a/webapp/src/app/pages/projects/projects.component.html b/webapp/src/app/pages/projects/projects.component.html new file mode 100644 index 0000000..662dfcc --- /dev/null +++ b/webapp/src/app/pages/projects/projects.component.html @@ -0,0 +1,26 @@ +<div class="row"> + <div class="col-12"> + <nb-card-body> + <div class="col-9"> + <nb-actions size="medium"> + <nb-action> + <button (click)="add()"> + <i class="nb-plus"></i> + <span>Add Project</span> + </button> + </nb-action> + </nb-actions> + </div> + <div class="col-3 right"> + <nb-actions size="medium"> + <nb-action> + <nb-search type="rotate-layout"></nb-search> + </nb-action> + </nb-actions> + </div> + </nb-card-body> + </div> + <div class="col-md-12 col-lg-12 col-xxxl-6" *ngFor="let prj of (projects$ | async)"> + <xds-project-card [project]="prj"></xds-project-card> + </div> +</div> diff --git a/webapp/src/app/pages/projects/projects.component.scss b/webapp/src/app/pages/projects/projects.component.scss new file mode 100644 index 0000000..3631fbb --- /dev/null +++ b/webapp/src/app/pages/projects/projects.component.scss @@ -0,0 +1,83 @@ +@import '../../@theme/styles/themes'; +@import '~@nebular/theme/components/card/card.component.theme'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; +@include nb-install-component() { + nb-card-body { + display: flex; + align-items: center; + } + .action-groups-header { + flex-basis: 20%; + color: nb-theme(card-header-fg-heading); + font-family: nb-theme(card-header-font-family); + font-size: nb-theme(card-header-font-size); + font-weight: nb-theme(card-header-font-weight); + } + .nb-actions { + flex-basis: 80%; + } + .right { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + order: 1; + flex-direction: row-reverse; + } + nb-actions>nb-action { + padding: 0; + } + nb-action { + i { + color: nb-theme(color-fg); + font-size: 2.5rem; + margin-right: 1rem; + } + span { + font-family: nb-theme(font-secondary); + font-weight: nb-theme(font-weight-bold); + color: nb-theme(color-fg-heading); + text-transform: uppercase; + } + button { + margin: 0 auto; + padding: 0; + cursor: pointer; + border: none; + background: none; + display: flex; + align-items: center; + &:focus { + box-shadow: none; + outline: none; + } + } + } + @include media-breakpoint-down(md) { + nb-actions nb-action { + padding: 0 0.75rem; + } + } + @include media-breakpoint-down(sm) { + nb-card-body { + padding: 1rem; + } + nb-action { + font-size: 0.75rem; + i { + font-size: 2rem; + margin-right: 0.5rem; + } + } + } + @include media-breakpoint-down(is) { + nb-action i { + font-size: 1.75rem; + margin: 0; + } + span { + display: none; + } + } +} diff --git a/webapp/src/app/pages/projects/projects.component.ts b/webapp/src/app/pages/projects/projects.component.ts new file mode 100644 index 0000000..179bbd0 --- /dev/null +++ b/webapp/src/app/pages/projects/projects.component.ts @@ -0,0 +1,33 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ProjectAddModalComponent } from './project-add-modal/project-add-modal.component'; + +import { ProjectService, IProject } from '../../@core-xds/services/project.service'; + +@Component({ + selector: 'xds-projects', + styleUrls: ['./projects.component.scss'], + templateUrl: './projects.component.html', +}) +export class ProjectsComponent implements OnInit { + + projects$: Observable<IProject[]>; + projects: IProject[]; + + constructor( + private projectSvr: ProjectService, + private modalService: NgbModal, + ) { + } + + ngOnInit() { + this.projects$ = this.projectSvr.Projects$; + } + + add() { + const activeModal = this.modalService.open(ProjectAddModalComponent, { size: 'lg', container: 'nb-layout' }); + activeModal.componentInstance.modalHeader = 'Large Modal'; + } +} diff --git a/webapp/src/app/pages/projects/projects.module.ts b/webapp/src/app/pages/projects/projects.module.ts new file mode 100644 index 0000000..48f37ce --- /dev/null +++ b/webapp/src/app/pages/projects/projects.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { ThemeModule } from '../../@theme/theme.module'; + +import { ProjectsComponent } from './projects.component'; +import { ProjectCardComponent, ProjectReadableTypePipe } from './project-card/project-card.component'; +import { ProjectAddModalComponent } from './project-add-modal/project-add-modal.component'; + + +@NgModule({ + imports: [ + ThemeModule, + ], + declarations: [ + ProjectsComponent, + ProjectCardComponent, + ProjectAddModalComponent, + ProjectReadableTypePipe, + ], + entryComponents: [ + ProjectAddModalComponent + ], +}) +export class ProjectsModule { } diff --git a/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.html b/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.html new file mode 100644 index 0000000..0c2787c --- /dev/null +++ b/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.html @@ -0,0 +1,48 @@ +<nb-card class="xds-sdks"> + <nb-card-header> + + <div class="row"> + <div class="col-12 col-md-8"> + {{ labelGet(sdk) }} + </div> + <div class="col-6 col-md-4 text-right" role="group"> + <button class="btn btn-outline-danger btn-tn btn-xds" (click)="delete(sdk)"> + <span class="fa fa-trash fa-size-x2"></span> + </button> + </div> + </div> + </nb-card-header> + + <nb-card-body> + <table class="table table-striped"> + <tbody> + <tr> + <th> + <span class="fa fa-fw fa-id-badge"></span> + <span>SDK ID</span> + </th> + <td>{{ sdk.id }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-user"></span> <span>Profile</span></th> + <td>{{ sdk.profile }}</td> + </tr> <tr> + <th><span class="fa fa-fw fa-tasks"></span> <span>Architecture</span></th> + <td>{{ sdk.arch }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-code-fork"></span> <span>Version</span></th> + <td>{{ sdk.version }}</td> + </tr> + <tr> + <th><span class="fa fa-fw fa-folder-open-o"></span> <span>Sdk path</span></th> + <td>{{ sdk.path}}</td> + </tr> + </tbody> + </table> + </nb-card-body> + + <nb-card-footer> + <!-- <pre>{{sdk | json}}</pre> --> + </nb-card-footer> +</nb-card> diff --git a/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.scss b/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.scss new file mode 100644 index 0000000..e404610 --- /dev/null +++ b/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.scss @@ -0,0 +1,47 @@ +@import '~@nebular/theme/styles/global/bootstrap/buttons'; + +.xds-sdk-card .icon { + padding: 0.75rem 0; + font-size: 1.75rem; +} + +nb-card-body { + padding: 0; +} + +nb-card-footer { + text-align: right; +} + +.fa-big { + font-size: 20px; + font-weight: bold; +} + +.fa-size-x2 { + font-size: 20px; +} + +th span { + font-weight: 100; +} + +th label { + font-weight: 100; + margin-bottom: 0; +} + +tr.info>th { + vertical-align: middle; +} + +tr.info>td { + vertical-align: middle; +} + +.btn-outline-danger.btn-xds { + color: #ff4c6a; + &:focus { + color: white; + } +} diff --git a/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.ts b/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.ts new file mode 100644 index 0000000..8228570 --- /dev/null +++ b/webapp/src/app/pages/sdks/sdk-card/sdk-card.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, Pipe, PipeTransform } from '@angular/core'; +import { SdkService, ISdk } from '../../../@core-xds/services/sdk.service'; +import { AlertService } from '../../../@core-xds/services/alert.service'; + + +@Component({ + selector: 'xds-sdk-card', + styleUrls: ['./sdk-card.component.scss'], + templateUrl: './sdk-card.component.html', +}) +export class SdkCardComponent { + + // FIXME workaround of https://github.com/angular/angular-cli/issues/2034 + // should be removed with angular 5 + // @Input() sdk: ISdk; + @Input() sdk = <ISdk>null; + + constructor( + private alert: AlertService, + private sdkSvr: SdkService + ) { + } + + labelGet(sdk: ISdk) { + return sdk.profile + '-' + sdk.arch + '-' + sdk.version; + } + + delete(sdk: ISdk) { + this.sdkSvr.delete(sdk).subscribe( + res => { }, + err => this.alert.error('ERROR delete: ' + err) + ); + } +} + diff --git a/webapp/src/app/pages/sdks/sdks.component.html b/webapp/src/app/pages/sdks/sdks.component.html new file mode 100644 index 0000000..adfd924 --- /dev/null +++ b/webapp/src/app/pages/sdks/sdks.component.html @@ -0,0 +1,26 @@ +<div class="row"> + <div class="col-12"> + <nb-card-body> + <div class="col-9"> + <nb-actions size="medium"> + <nb-action> + <button (click)="add()"> + <i class="nb-plus"></i> + <span>Add new SDK</span> + </button> + </nb-action> + </nb-actions> + </div> + <div class="col-3 right"> + <nb-actions size="medium"> + <nb-action> + <nb-search type="rotate-layout"></nb-search> + </nb-action> + </nb-actions> + </div> + </nb-card-body> + </div> + <div class="col-md-12 col-lg-12 col-xxxl-6" *ngFor="let sdk of (sdks$ | async)"> + <xds-sdk-card [sdk]="sdk"></xds-sdk-card> + </div> +</div> diff --git a/webapp/src/app/pages/sdks/sdks.component.scss b/webapp/src/app/pages/sdks/sdks.component.scss new file mode 100644 index 0000000..3631fbb --- /dev/null +++ b/webapp/src/app/pages/sdks/sdks.component.scss @@ -0,0 +1,83 @@ +@import '../../@theme/styles/themes'; +@import '~@nebular/theme/components/card/card.component.theme'; +@import '~bootstrap/scss/mixins/breakpoints'; +@import '~@nebular/theme/styles/global/bootstrap/breakpoints'; +@include nb-install-component() { + nb-card-body { + display: flex; + align-items: center; + } + .action-groups-header { + flex-basis: 20%; + color: nb-theme(card-header-fg-heading); + font-family: nb-theme(card-header-font-family); + font-size: nb-theme(card-header-font-size); + font-weight: nb-theme(card-header-font-weight); + } + .nb-actions { + flex-basis: 80%; + } + .right { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + order: 1; + flex-direction: row-reverse; + } + nb-actions>nb-action { + padding: 0; + } + nb-action { + i { + color: nb-theme(color-fg); + font-size: 2.5rem; + margin-right: 1rem; + } + span { + font-family: nb-theme(font-secondary); + font-weight: nb-theme(font-weight-bold); + color: nb-theme(color-fg-heading); + text-transform: uppercase; + } + button { + margin: 0 auto; + padding: 0; + cursor: pointer; + border: none; + background: none; + display: flex; + align-items: center; + &:focus { + box-shadow: none; + outline: none; + } + } + } + @include media-breakpoint-down(md) { + nb-actions nb-action { + padding: 0 0.75rem; + } + } + @include media-breakpoint-down(sm) { + nb-card-body { + padding: 1rem; + } + nb-action { + font-size: 0.75rem; + i { + font-size: 2rem; + margin-right: 0.5rem; + } + } + } + @include media-breakpoint-down(is) { + nb-action i { + font-size: 1.75rem; + margin: 0; + } + span { + display: none; + } + } +} diff --git a/webapp/src/app/pages/sdks/sdks.component.ts b/webapp/src/app/pages/sdks/sdks.component.ts new file mode 100644 index 0000000..3208121 --- /dev/null +++ b/webapp/src/app/pages/sdks/sdks.component.ts @@ -0,0 +1,35 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; + +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +//import { SdkAddModalComponent } from './sdk-add-modal/sdk-add-modal.component'; + +import { SdkService, ISdk } from '../../@core-xds/services/sdk.service'; + +@Component({ + selector: 'xds-sdks', + styleUrls: ['./sdks.component.scss'], + templateUrl: './sdks.component.html', +}) +export class SdksComponent implements OnInit { + + sdks$: Observable<ISdk[]>; + sdks: ISdk[]; + + constructor( + private sdkSvr: SdkService, + private modalService: NgbModal, + ) { + } + + ngOnInit() { + this.sdks$ = this.sdkSvr.Sdks$; + } + + add() { + /* SEB TODO + const activeModal = this.modalService.open(SdkAddModalComponent, { size: 'lg', container: 'nb-layout' }); + activeModal.componentInstance.modalHeader = 'Large Modal'; + */ + } +} diff --git a/webapp/src/app/pages/sdks/sdks.module.ts b/webapp/src/app/pages/sdks/sdks.module.ts new file mode 100644 index 0000000..48629f6 --- /dev/null +++ b/webapp/src/app/pages/sdks/sdks.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from '@angular/core'; +import { ThemeModule } from '../../@theme/theme.module'; + +import { SdksComponent } from './sdks.component'; +import { SdkCardComponent } from './sdk-card/sdk-card.component'; +//import { SdkAddModalComponent } from './sdk-add-modal/sdk-add-modal.component'; + + +@NgModule({ + imports: [ + ThemeModule, + ], + declarations: [ + SdksComponent, + SdkCardComponent, + //SdkAddModalComponent, + ], + entryComponents: [ + //SdkAddModalComponent + ], +}) +export class SdksModule { } diff --git a/webapp/src/app/projects/projectAddModal.component.css b/webapp/src/app/projects/projectAddModal.component.css deleted file mode 100644 index 77f73a5..0000000 --- a/webapp/src/app/projects/projectAddModal.component.css +++ /dev/null @@ -1,24 +0,0 @@ -.table-borderless>tbody>tr>td, -.table-borderless>tbody>tr>th, -.table-borderless>tfoot>tr>td, -.table-borderless>tfoot>tr>th, -.table-borderless>thead>tr>td, -.table-borderless>thead>tr>th { - border: none; -} - -tr>th { - vertical-align: middle; -} - -tr>td { - vertical-align: middle; -} - -th label { - margin-bottom: 0; -} - -td input { - width: 100%; -} diff --git a/webapp/src/app/projects/projectAddModal.component.html b/webapp/src/app/projects/projectAddModal.component.html deleted file mode 100644 index dc84985..0000000 --- a/webapp/src/app/projects/projectAddModal.component.html +++ /dev/null @@ -1,54 +0,0 @@ -<div bsModal #childProjectModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" - [config]="{backdrop: 'static'}" aria-hidden="true"> - <div class="modal-dialog modal-lg"> - <div class="modal-content"> - <div class="modal-header"> - <h4 class="modal-title pull-left">{{title}}</h4> - <button type="button" class="close pull-right" aria-label="Close" (click)="hide()"> - <span aria-hidden="true">×</span> - </button> - </div> - - <form [formGroup]="addProjectForm" (ngSubmit)="onSubmit()"> - <div class="modal-body"> - <div class="row "> - <div class="col-xs-12"> - <table class="table table-borderless"> - <tbody> - <tr> - <th><label>Sharing Type </label></th> - <td><select class="form-control" formControlName="type"> - <option *ngFor="let t of projectTypes" [value]="t.value">{{t.display}} - </option> - </select> - </td> - </tr> - <tr> - <th><label for="select-local-path">Local Path </label></th> - <td><input type="text" id="select-local-path" formControlName="pathCli" placeholder="/tmp/myProject" (change)="onChangeLocalProject($event)"></td> - </tr> - <tr> - <th><label for="select-server-path">Server Path </label></th> - <td><input type="text" id="select-server-path" formControlName="pathSvr"></td> - </tr> - <tr> - <th><label for="select-label">Label </label></th> - <td><input type="text" formControlName="label" id="select-label" (keyup)="onKeyLabel($event)"></td> - </tr> - </tbody> - </table> - </div> - </div> - </div> - <div class="modal-footer"> - <div class="pull-left"> - <button class="btn btn-default" (click)="cancelAction=true; hide()"> Cancel </button> - </div> - <div class=""> - <button class="btn btn-primary" type="submit" [disabled]="!addProjectForm.valid">Add Folder</button> - </div> - </div> - </form> - </div> - </div> -</div> diff --git a/webapp/src/app/projects/projectAddModal.component.ts b/webapp/src/app/projects/projectAddModal.component.ts deleted file mode 100644 index 0718f4c..0000000 --- a/webapp/src/app/projects/projectAddModal.component.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Component, Input, ViewChild, OnInit } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { ModalDirective } from 'ngx-bootstrap/modal'; -import { FormControl, FormGroup, Validators, FormBuilder, ValidatorFn, AbstractControl } from '@angular/forms'; - -// Import RxJs required methods -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/filter'; -import 'rxjs/add/operator/debounceTime'; - -import { AlertService, IAlert } from '../services/alert.service'; -import { ProjectService, IProject, ProjectType, ProjectTypes } from '../services/project.service'; - - -@Component({ - selector: 'xds-project-add-modal', - templateUrl: 'projectAddModal.component.html', - styleUrls: ['projectAddModal.component.css'] -}) -export class ProjectAddModalComponent implements OnInit { - @ViewChild('childProjectModal') public childProjectModal: ModalDirective; - @Input() title?: string; - @Input('server-id') serverID: string; - - cancelAction = false; - userEditedLabel = false; - projectTypes = ProjectTypes; - - addProjectForm: FormGroup; - typeCtrl: FormControl; - pathCliCtrl: FormControl; - pathSvrCtrl: FormControl; - - constructor( - private alert: AlertService, - private projectSvr: ProjectService, - private fb: FormBuilder - ) { - // Define types (first one is special/placeholder) - this.projectTypes.unshift({ value: ProjectType.UNSET, display: '--Select a type--' }); - - this.typeCtrl = new FormControl(this.projectTypes[0].value, Validators.pattern('[A-Za-z]+')); - this.pathCliCtrl = new FormControl('', Validators.required); - this.pathSvrCtrl = new FormControl({ value: '', disabled: true }, [Validators.required, Validators.minLength(1)]); - - this.addProjectForm = fb.group({ - type: this.typeCtrl, - pathCli: this.pathCliCtrl, - pathSvr: this.pathSvrCtrl, - label: ['', Validators.nullValidator], - }); - } - - ngOnInit() { - // Auto create label name - this.pathCliCtrl.valueChanges - .debounceTime(100) - .filter(n => n) - .map(n => { - const last = n.split('/'); - let nm = n; - if (last.length > 0) { - nm = last.pop(); - if (nm === '' && last.length > 0) { - nm = last.pop(); - } - } - return 'Project_' + nm; - }) - .subscribe(value => { - if (value && !this.userEditedLabel) { - this.addProjectForm.patchValue({ label: value }); - } - }); - - // Handle disabling of Server path - this.typeCtrl.valueChanges - .debounceTime(500) - .subscribe(valType => { - const dis = (valType === String(ProjectType.SYNCTHING)); - this.pathSvrCtrl.reset({ value: '', disabled: dis }); - }); - } - - show() { - this.cancelAction = false; - this.userEditedLabel = false; - this.childProjectModal.show(); - } - - hide() { - this.childProjectModal.hide(); - } - - onKeyLabel(event: any) { - this.userEditedLabel = (this.addProjectForm.value.label !== ''); - } - - /* FIXME: change input to file type - <td><input type='file' id='select-local-path' webkitdirectory - formControlName='pathCli' placeholder='myProject' (change)='onChangeLocalProject($event)'></td> - - onChangeLocalProject(e) { - if e.target.files.length < 1 { - console.log('NO files'); - } - let dir = e.target.files[0].webkitRelativePath; - console.log('files: ' + dir); - let u = URL.createObjectURL(e.target.files[0]); - } - */ - onChangeLocalProject(e) { - } - - onSubmit() { - if (this.cancelAction) { - return; - } - - const formVal = this.addProjectForm.value; - - const type = formVal['type'].value; - this.projectSvr.Add({ - serverId: this.serverID, - label: formVal['label'], - pathClient: formVal['pathCli'], - pathServer: formVal['pathSvr'], - type: formVal['type'], - // FIXME: allow to set defaultSdkID from New Project config panel - }) - .subscribe(prj => { - this.alert.info('Project ' + prj.label + ' successfully created.'); - this.hide(); - - // Reset Value for the next creation - this.addProjectForm.reset(); - const selectedType = this.projectTypes[0].value; - this.addProjectForm.patchValue({ type: selectedType }); - - }, - err => { - this.alert.error(err, 60); - this.hide(); - }); - } - -} diff --git a/webapp/src/app/projects/projectCard.component.ts b/webapp/src/app/projects/projectCard.component.ts deleted file mode 100644 index a28b96c..0000000 --- a/webapp/src/app/projects/projectCard.component.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Component, Input, Pipe, PipeTransform } from '@angular/core'; -import { ProjectService, IProject, ProjectType } from '../services/project.service'; -import { AlertService } from '../services/alert.service'; - -@Component({ - selector: 'xds-project-card', - template: ` - <div class="row"> - <div class="col-xs-12"> - <div class="text-right" role="group"> - <button class="btn btn-link" (click)="delete(project)"> - <span class="fa fa-trash fa-size-x2"></span> - </button> - </div> - </div> - </div> - - <table class="table table-striped"> - <tbody> - <tr> - <th><span class="fa fa-fw fa-id-badge"></span> <span>Project ID</span></th> - <td>{{ project.id }}</td> - </tr> - <tr> - <th><span class="fa fa-fw fa-exchange"></span> <span>Sharing type</span></th> - <td>{{ project.type | readableType }}</td> - </tr> - <tr> - <th><span class="fa fa-fw fa-folder-open-o"></span> <span>Local path</span></th> - <td>{{ project.pathClient }}</td> - </tr> - <tr *ngIf="project.pathServer && project.pathServer != ''"> - <th><span class="fa fa-fw fa-folder-open-o"></span> <span>Server path</span></th> - <td>{{ project.pathServer }}</td> - </tr> - <tr> - <th><span class="fa fa-fw fa-flag"></span> <span>Status</span></th> - <td>{{ project.status }} - {{ project.isInSync ? "Up to Date" : "Out of Sync"}} - <button *ngIf="!project.isInSync" class="btn btn-link" (click)="sync(project)"> - <span class="fa fa-refresh fa-size-x2"></span> - </button> - </td> - </tr> - </tbody> - </table > - `, - styleUrls: ['../config/config.component.css'] -}) - -export class ProjectCardComponent { - - @Input() project: IProject; - - constructor( - private alert: AlertService, - private projectSvr: ProjectService - ) { - } - - delete(prj: IProject) { - this.projectSvr.Delete(prj) - .subscribe(res => { - }, err => { - this.alert.error('Delete ERROR: ' + err); - }); - } - - sync(prj: IProject) { - this.projectSvr.Sync(prj) - .subscribe(res => { - }, err => { - this.alert.error('ERROR: ' + err); - }); - } - -} - -// Remove APPS. prefix if translate has failed -@Pipe({ - name: 'readableType' -}) - -export class ProjectReadableTypePipe implements PipeTransform { - transform(type: ProjectType): string { - switch (type) { - case ProjectType.NATIVE_PATHMAP: return 'Native (path mapping)'; - case ProjectType.SYNCTHING: return 'Cloud (Syncthing)'; - default: return String(type); - } - } -} diff --git a/webapp/src/app/projects/projectsListAccordion.component.ts b/webapp/src/app/projects/projectsListAccordion.component.ts deleted file mode 100644 index 0dd2f12..0000000 --- a/webapp/src/app/projects/projectsListAccordion.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Component, Input } from '@angular/core'; - -import { IProject } from '../services/project.service'; - -@Component({ - selector: 'xds-projects-list-accordion', - template: ` - <style> - .fa.fa-exclamation-triangle { - margin-right: 2em; - color: red; - } - .fa.fa-refresh { - margin-right: 10px; - color: darkviolet; - } - </style> - <accordion> - <accordion-group #group *ngFor="let prj of projects"> - <div accordion-heading> - {{ prj.label }} - <div class="pull-right"> - <i *ngIf="prj.status == 'Syncing'" class="fa fa-refresh faa-spin animated"></i> - <i *ngIf="!prj.isInSync && prj.status != 'Syncing'" class="fa fa-exclamation-triangle"></i> - <i class="fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i> - </div> - </div> - <xds-project-card [project]="prj"></xds-project-card> - </accordion-group> - </accordion> - ` -}) -export class ProjectsListAccordionComponent { - - @Input() projects: IProject[]; - -} - - diff --git a/webapp/src/app/sdks/sdkAddModal.component.html b/webapp/src/app/sdks/sdkAddModal.component.html deleted file mode 100644 index 44c667e..0000000 --- a/webapp/src/app/sdks/sdkAddModal.component.html +++ /dev/null @@ -1,23 +0,0 @@ -<div bsModal #sdkChildModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" - aria-hidden="true"> - <div class="modal-dialog modal-lg"> - <div class="modal-content"> - <div class="modal-header"> - <h4 class="modal-title pull-left">{{title}}</h4> - <button type="button" class="close pull-right" aria-label="Close" (click)="hide()"> - <span aria-hidden="true">×</span> - </button> - </div> - <div class="modal-body"> - <ng-content select=".modal-body"> </ng-content> - <i>Not available for now.</i> - </div> - - <div class="modal-footer"> - <div class="pull-left"> - <button class="btn btn-default" (click)="hide()"> Cancel </button> - </div> - </div> - </div> - </div> -</div> diff --git a/webapp/src/app/sdks/sdkAddModal.component.ts b/webapp/src/app/sdks/sdkAddModal.component.ts deleted file mode 100644 index 064f02f..0000000 --- a/webapp/src/app/sdks/sdkAddModal.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ModalDirective } from 'ngx-bootstrap/modal'; - -@Component({ - selector: 'xds-sdk-add-modal', - templateUrl: 'sdkAddModal.component.html', -}) -export class SdkAddModalComponent { - @ViewChild('sdkChildModal') public sdkChildModal: ModalDirective; - - @Input() title?: string; - - // TODO - constructor() { - } - - show() { - this.sdkChildModal.show(); - } - - hide() { - this.sdkChildModal.hide(); - } -} diff --git a/webapp/src/app/sdks/sdkCard.component.ts b/webapp/src/app/sdks/sdkCard.component.ts deleted file mode 100644 index b277887..0000000 --- a/webapp/src/app/sdks/sdkCard.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { ISdk } from '../services/sdk.service'; - -@Component({ - selector: 'xds-sdk-card', - template: ` - <div class="row"> - <div class="col-xs-12"> - <div class="text-right" role="group"> - <button disabled class="btn btn-link" (click)="delete(sdk)"><span class="fa fa-trash fa-size-x2"></span></button> - </div> - </div> - </div> - - <table class="table table-striped"> - <tbody> - <tr> - <th><span class="fa fa-fw fa-id-badge"></span> <span>SDK ID</span></th> - <td>{{ sdk.id }}</td> - </tr> - <tr> - <th><span class="fa fa-fw fa-user"></span> <span>Profile</span></th> - <td>{{ sdk.profile }}</td> - </tr> - <tr> - <th><span class="fa fa-fw fa-tasks"></span> <span>Architecture</span></th> - <td>{{ sdk.arch }}</td> - </tr> - <tr> - <th><span class="fa fa-fw fa-code-fork"></span> <span>Version</span></th> - <td>{{ sdk.version }}</td> - </tr> - <tr> - <th><span class="fa fa-fw fa-folder-open-o"></span> <span>Sdk path</span></th> - <td>{{ sdk.path}}</td> - </tr> - - </tbody> - </table > - `, - styleUrls: ['../config/config.component.css'] -}) - -export class SdkCardComponent { - - @Input() sdk: ISdk; - - constructor() { } - - - delete(sdk: ISdk) { - // Not supported for now - } - -} diff --git a/webapp/src/app/sdks/sdksListAccordion.component.ts b/webapp/src/app/sdks/sdksListAccordion.component.ts deleted file mode 100644 index 73ad182..0000000 --- a/webapp/src/app/sdks/sdksListAccordion.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component, Input } from '@angular/core'; - -import { ISdk } from '../services/sdk.service'; - -@Component({ - selector: 'xds-sdks-list-accordion', - template: ` - <accordion> - <accordion-group #group *ngFor="let sdk of sdks"> - <div accordion-heading> - {{ sdk.name }} - <i class="pull-right float-xs-right fa" - [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i> - </div> - <xds-sdk-card [sdk]="sdk"></xds-sdk-card> - </accordion-group> - </accordion> - ` -}) -export class SdksListAccordionComponent { - - @Input() sdks: ISdk[]; - -} - - diff --git a/webapp/src/app/services/alert.service.ts b/webapp/src/app/services/alert.service.ts deleted file mode 100644 index aee5827..0000000 --- a/webapp/src/app/services/alert.service.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Injectable, SecurityContext } from '@angular/core'; -import { DomSanitizer } from '@angular/platform-browser'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; - - -export type AlertType = 'danger' | 'warning' | 'info' | 'success'; - -export interface IAlert { - type: AlertType; - msg: string; - show?: boolean; - dismissible?: boolean; - dismissTimeout?: number; // close alert after this time (in seconds) - id?: number; -} - -@Injectable() -export class AlertService { - public alerts: Observable<IAlert[]>; - - private _alerts: IAlert[]; - private alertsSubject = <Subject<IAlert[]>>new Subject(); - private uid = 0; - private defaultDismissTmo = 5; // in seconds - - constructor(private sanitizer: DomSanitizer) { - this.alerts = this.alertsSubject.asObservable(); - this._alerts = []; - this.uid = 0; - } - - public error(msg: string, dismissTime?: number) { - this.add({ - type: 'danger', msg: msg, dismissible: true, dismissTimeout: dismissTime - }); - } - - public warning(msg: string, dismissible?: boolean) { - this.add({ type: 'warning', msg: msg, dismissible: true, dismissTimeout: (dismissible ? this.defaultDismissTmo : 0) }); - } - - public info(msg: string) { - this.add({ type: 'info', msg: msg, dismissible: true, dismissTimeout: this.defaultDismissTmo }); - } - - public add(al: IAlert) { - const msg = String(al.msg).replace('\n', '<br>'); - this._alerts.push({ - show: true, - type: al.type, - msg: this.sanitizer.sanitize(SecurityContext.HTML, msg), - dismissible: al.dismissible || true, - dismissTimeout: (al.dismissTimeout * 1000) || 0, - id: this.uid, - }); - this.uid += 1; - this.alertsSubject.next(this._alerts); - } - - public del(al: IAlert) { - const idx = this._alerts.findIndex((a) => a.id === al.id); - if (idx > -1) { - this._alerts.splice(idx, 1); - } - } -} diff --git a/webapp/src/app/services/project.service.ts b/webapp/src/app/services/project.service.ts deleted file mode 100644 index 61d8f1c..0000000 --- a/webapp/src/app/services/project.service.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { Injectable, SecurityContext } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; - -import { XDSAgentService, IXDSProjectConfig } from '../services/xdsagent.service'; - -export enum ProjectType { - UNSET = '', - NATIVE_PATHMAP = 'PathMap', - SYNCTHING = 'CloudSync' -} - -export const ProjectTypes = [ - { value: ProjectType.NATIVE_PATHMAP, display: 'Path mapping' }, - { value: ProjectType.SYNCTHING, display: 'Cloud Sync' } -]; - -export const ProjectStatus = { - ErrorConfig: 'ErrorConfig', - Disable: 'Disable', - Enable: 'Enable', - Pause: 'Pause', - Syncing: 'Syncing' -}; - -export interface IProject { - id?: string; - serverId: string; - label: string; - pathClient: string; - pathServer?: string; - type: ProjectType; - status?: string; - isInSync?: boolean; - isUsable?: boolean; - serverPrjDef?: IXDSProjectConfig; - isExpanded?: boolean; - visible?: boolean; - defaultSdkID?: string; -} - -@Injectable() -export class ProjectService { - public Projects$: Observable<IProject[]>; - - private _prjsList: IProject[] = []; - private current: IProject; - private prjsSubject = <BehaviorSubject<IProject[]>>new BehaviorSubject(this._prjsList); - - constructor(private xdsSvr: XDSAgentService) { - this.current = null; - this.Projects$ = this.prjsSubject.asObservable(); - - this.xdsSvr.getProjects().subscribe((projects) => { - this._prjsList = []; - projects.forEach(p => { - this._addProject(p, true); - }); - this.prjsSubject.next(Object.assign([], this._prjsList)); - }); - - // Update Project data - this.xdsSvr.ProjectState$.subscribe(prj => { - const i = this._getProjectIdx(prj.id); - if (i >= 0) { - // XXX for now, only isInSync and status may change - this._prjsList[i].isInSync = prj.isInSync; - this._prjsList[i].status = prj.status; - this._prjsList[i].isUsable = this._isUsableProject(prj); - this.prjsSubject.next(Object.assign([], this._prjsList)); - } - }); - - // Add listener on create and delete project events - this.xdsSvr.addEventListener('event:project-add', (ev) => { - if (ev && ev.data && ev.data.id) { - this._addProject(ev.data); - } else { - console.log('Warning: received events with unknown data: ev=', ev); - } - }); - this.xdsSvr.addEventListener('event:project-delete', (ev) => { - if (ev && ev.data && ev.data.id) { - const idx = this._prjsList.findIndex(item => item.id === ev.data.id); - if (idx === -1) { - console.log('Warning: received events on unknown project id: ev=', ev); - return; - } - this._prjsList.splice(idx, 1); - this.prjsSubject.next(Object.assign([], this._prjsList)); - } else { - console.log('Warning: received events with unknown data: ev=', ev); - } - }); - - } - - public setCurrent(s: IProject) { - this.current = s; - } - - public getCurrent(): IProject { - return this.current; - } - - public getCurrentId(): string { - if (this.current && this.current.id) { - return this.current.id; - } - return ''; - } - - Add(prj: IProject): Observable<IProject> { - const xdsPrj: IXDSProjectConfig = { - id: '', - serverId: prj.serverId, - label: prj.label || '', - clientPath: prj.pathClient.trim(), - serverPath: prj.pathServer, - type: prj.type, - defaultSdkID: prj.defaultSdkID, - }; - // Send config to XDS server - return this.xdsSvr.addProject(xdsPrj) - .map(xp => this._convToIProject(xp)); - } - - Delete(prj: IProject): Observable<IProject> { - const idx = this._getProjectIdx(prj.id); - const delPrj = prj; - if (idx === -1) { - throw new Error('Invalid project id (id=' + prj.id + ')'); - } - return this.xdsSvr.deleteProject(prj.id) - .map(res => delPrj); - } - - Sync(prj: IProject): Observable<string> { - const idx = this._getProjectIdx(prj.id); - if (idx === -1) { - throw new Error('Invalid project id (id=' + prj.id + ')'); - } - return this.xdsSvr.syncProject(prj.id); - } - - private _isUsableProject(p) { - return p && p.isInSync && - (p.status === ProjectStatus.Enable) && - (p.status !== ProjectStatus.Syncing); - } - - private _getProjectIdx(id: string): number { - return this._prjsList.findIndex((item) => item.id === id); - } - - private _convToIProject(rPrj: IXDSProjectConfig): IProject { - // Convert XDSFolderConfig to IProject - const pp: IProject = { - id: rPrj.id, - serverId: rPrj.serverId, - label: rPrj.label, - pathClient: rPrj.clientPath, - pathServer: rPrj.serverPath, - type: rPrj.type, - status: rPrj.status, - isInSync: rPrj.isInSync, - isUsable: this._isUsableProject(rPrj), - defaultSdkID: rPrj.defaultSdkID, - serverPrjDef: Object.assign({}, rPrj), // do a copy - }; - return pp; - } - - private _addProject(rPrj: IXDSProjectConfig, noNext?: boolean): IProject { - - // Convert XDSFolderConfig to IProject - const pp = this._convToIProject(rPrj); - - // add new project - this._prjsList.push(pp); - - // sort project array - this._prjsList.sort((a, b) => { - if (a.label < b.label) { - return -1; - } - if (a.label > b.label) { - return 1; - } - return 0; - }); - - if (!noNext) { - this.prjsSubject.next(Object.assign([], this._prjsList)); - } - - return pp; - } -} diff --git a/webapp/src/app/services/utils.service.ts b/webapp/src/app/services/utils.service.ts deleted file mode 100644 index e665e2a..0000000 --- a/webapp/src/app/services/utils.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable() -export class UtilsService { - constructor() { } - - getOSName(lowerCase?: boolean): string { - const checkField = function (ff) { - if (ff.indexOf('Linux') !== -1) { - return 'Linux'; - } else if (ff.indexOf('Win') !== -1) { - return 'Windows'; - } else if (ff.indexOf('Mac') !== -1) { - return 'MacOS'; - } else if (ff.indexOf('X11') !== -1) { - return 'UNIX'; - } - return ''; - }; - - let OSName = checkField(navigator.platform); - if (OSName === '') { - OSName = checkField(navigator.appVersion); - } - if (OSName === '') { - OSName = 'Unknown OS'; - } - if (lowerCase) { - return OSName.toLowerCase(); - } - return OSName; - } -} diff --git a/webapp/src/app/services/xdsagent.service.ts b/webapp/src/app/services/xdsagent.service.ts deleted file mode 100644 index 55653c7..0000000 --- a/webapp/src/app/services/xdsagent.service.ts +++ /dev/null @@ -1,384 +0,0 @@ -import { Injectable, Inject } from '@angular/core'; -import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; -import { DOCUMENT } from '@angular/common'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import * as io from 'socket.io-client'; - -import { AlertService } from './alert.service'; -import { ISdk } from './sdk.service'; -import { ProjectType } from './project.service'; - -// Import RxJs required methods -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/catch'; -import 'rxjs/add/observable/throw'; -import 'rxjs/add/operator/mergeMap'; -import 'rxjs/add/observable/of'; -import 'rxjs/add/operator/retryWhen'; - - -export interface IXDSConfigProject { - id: string; - path: string; - clientSyncThingID: string; - type: string; - label?: string; - defaultSdkID?: string; -} - -interface IXDSBuilderConfig { - ip: string; - port: string; - syncThingID: string; -} - -export interface IXDSProjectConfig { - id: string; - serverId: string; - label: string; - clientPath: string; - serverPath?: string; - type: ProjectType; - status?: string; - isInSync?: boolean; - defaultSdkID: string; -} - -export interface IXDSVer { - id: string; - version: string; - apiVersion: string; - gitTag: string; -} - -export interface IXDSVersions { - client: IXDSVer; - servers: IXDSVer[]; -} - -export interface IXDServerCfg { - id: string; - url: string; - apiUrl: string; - partialUrl: string; - connRetry: number; - connected: boolean; -} - -export interface IXDSConfig { - servers: IXDServerCfg[]; -} - -export interface ISdkMessage { - wsID: string; - msgType: string; - data: any; -} - -export interface ICmdOutput { - cmdID: string; - timestamp: string; - stdout: string; - stderr: string; -} - -export interface ICmdExit { - cmdID: string; - timestamp: string; - code: number; - error: string; -} - -export interface IAgentStatus { - WS_connected: boolean; -} - - -@Injectable() -export class XDSAgentService { - - public XdsConfig$: Observable<IXDSConfig>; - public Status$: Observable<IAgentStatus>; - public ProjectState$ = <Subject<IXDSProjectConfig>>new Subject(); - public CmdOutput$ = <Subject<ICmdOutput>>new Subject(); - public CmdExit$ = <Subject<ICmdExit>>new Subject(); - - private baseUrl: string; - private wsUrl: string; - private _config = <IXDSConfig>{ servers: [] }; - private _status = { WS_connected: false }; - - private configSubject = <BehaviorSubject<IXDSConfig>>new BehaviorSubject(this._config); - private statusSubject = <BehaviorSubject<IAgentStatus>>new BehaviorSubject(this._status); - - private socket: SocketIOClient.Socket; - - constructor( @Inject(DOCUMENT) private document: Document, - private http: HttpClient, private alert: AlertService) { - - this.XdsConfig$ = this.configSubject.asObservable(); - this.Status$ = this.statusSubject.asObservable(); - - const originUrl = this.document.location.origin; - this.baseUrl = originUrl + '/api/v1'; - - const re = originUrl.match(/http[s]?:\/\/([^\/]*)[\/]?/); - if (re === null || re.length < 2) { - console.error('ERROR: cannot determine Websocket url'); - } else { - this.wsUrl = 'ws://' + re[1]; - this._handleIoSocket(); - this._RegisterEvents(); - } - } - - private _WSState(sts: boolean) { - this._status.WS_connected = sts; - this.statusSubject.next(Object.assign({}, this._status)); - - // Update XDS config including XDS Server list when connected - if (sts) { - this.getConfig().subscribe(c => { - this._config = c; - this.configSubject.next( - Object.assign({ servers: [] }, this._config) - ); - }); - } - } - - private _handleIoSocket() { - this.socket = io(this.wsUrl, { transports: ['websocket'] }); - - this.socket.on('connect_error', (res) => { - this._WSState(false); - console.error('XDS Agent WebSocket Connection error !'); - }); - - this.socket.on('connect', (res) => { - this._WSState(true); - }); - - this.socket.on('disconnection', (res) => { - this._WSState(false); - this.alert.error('WS disconnection: ' + res); - }); - - this.socket.on('error', (err) => { - console.error('WS error:', err); - }); - - this.socket.on('make:output', data => { - this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data)); - }); - - this.socket.on('make:exit', data => { - this.CmdExit$.next(Object.assign({}, <ICmdExit>data)); - }); - - this.socket.on('exec:output', data => { - this.CmdOutput$.next(Object.assign({}, <ICmdOutput>data)); - }); - - this.socket.on('exec:exit', data => { - this.CmdExit$.next(Object.assign({}, <ICmdExit>data)); - }); - - // Events - // (project-add and project-delete events are managed by project.service) - this.socket.on('event:server-config', ev => { - if (ev && ev.data) { - const cfg: IXDServerCfg = ev.data; - const idx = this._config.servers.findIndex(el => el.id === cfg.id); - if (idx >= 0) { - this._config.servers[idx] = Object.assign({}, cfg); - } - this.configSubject.next(Object.assign({}, this._config)); - } - }); - - this.socket.on('event:project-state-change', ev => { - if (ev && ev.data) { - this.ProjectState$.next(Object.assign({}, ev.data)); - } - }); - - } - - /** - ** Events - ***/ - addEventListener(ev: string, fn: Function): SocketIOClient.Emitter { - return this.socket.addEventListener(ev, fn); - } - - /** - ** Misc / Version - ***/ - getVersion(): Observable<IXDSVersions> { - return this._get('/version'); - } - - /*** - ** Config - ***/ - getConfig(): Observable<IXDSConfig> { - return this._get('/config'); - } - - setConfig(cfg: IXDSConfig): Observable<IXDSConfig> { - return this._post('/config', cfg); - } - - setServerRetry(serverID: string, r: number) { - const svr = this._getServer(serverID); - if (!svr) { - return Observable.of([]); - } - - svr.connRetry = r; - this.setConfig(this._config).subscribe( - newCfg => { - this._config = newCfg; - this.configSubject.next(Object.assign({}, this._config)); - }, - err => { - this.alert.error(err); - } - ); - } - - setServerUrl(serverID: string, url: string) { - const svr = this._getServer(serverID); - if (!svr) { - return Observable.of([]); - } - svr.url = url; - this.setConfig(this._config).subscribe( - newCfg => { - this._config = newCfg; - this.configSubject.next(Object.assign({}, this._config)); - }, - err => { - this.alert.error(err); - } - ); - } - - /*** - ** SDKs - ***/ - getSdks(serverID: string): Observable<ISdk[]> { - const svr = this._getServer(serverID); - if (!svr || !svr.connected) { - return Observable.of([]); - } - - return this._get(svr.partialUrl + '/sdks'); - } - - /*** - ** Projects - ***/ - getProjects(): Observable<IXDSProjectConfig[]> { - return this._get('/projects'); - } - - addProject(cfg: IXDSProjectConfig): Observable<IXDSProjectConfig> { - return this._post('/projects', cfg); - } - - deleteProject(id: string): Observable<IXDSProjectConfig> { - return this._delete('/projects/' + id); - } - - syncProject(id: string): Observable<string> { - return this._post('/projects/sync/' + id, {}); - } - - /*** - ** Exec - ***/ - exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> { - return this._post('/exec', - { - id: prjID, - rpath: dir, - cmd: cmd, - sdkID: sdkid || '', - args: args || [], - env: env || [], - }); - } - - /** - ** Private functions - ***/ - - private _RegisterEvents() { - // Register to all existing events - this._post('/events/register', { 'name': 'event:all' }) - .subscribe( - res => { }, - error => { - this.alert.error('ERROR while registering to all events: ' + error); - } - ); - } - - private _getServer(ID: string): IXDServerCfg { - const svr = this._config.servers.filter(item => item.id === ID); - if (svr.length < 1) { - return null; - } - return svr[0]; - } - - private _attachAuthHeaders(options?: any) { - options = options || {}; - const headers = options.headers || new HttpHeaders(); - // headers.append('Authorization', 'Basic ' + btoa('username:password')); - headers.append('Accept', 'application/json'); - headers.append('Content-Type', 'application/json'); - // headers.append('Access-Control-Allow-Origin', '*'); - - options.headers = headers; - return options; - } - - private _get(url: string): Observable<any> { - return this.http.get(this.baseUrl + url, this._attachAuthHeaders()) - .catch(this._decodeError); - } - private _post(url: string, body: any): Observable<any> { - return this.http.post(this.baseUrl + url, JSON.stringify(body), this._attachAuthHeaders()) - .catch((error) => { - return this._decodeError(error); - }); - } - private _delete(url: string): Observable<any> { - return this.http.delete(this.baseUrl + url, this._attachAuthHeaders()) - .catch(this._decodeError); - } - - private _decodeError(err: any) { - let e: string; - if (err instanceof HttpErrorResponse) { - e = (err.error && err.error.error) ? err.error.error : err.message || 'Unknown error'; - } else if (typeof err === 'object') { - if (err.statusText) { - e = err.statusText; - } else if (err.error) { - e = String(err.error); - } else { - e = JSON.stringify(err); - } - } else { - e = err.message ? err.message : err.toString(); - } - console.log('xdsagent.service - ERROR: ', e); - return Observable.throw(e); - } -} |