From 799a5f6b1f3ed7ad1c71556ca5af5a0f9fabe60e Mon Sep 17 00:00:00 2001 From: Donghuang Date: Tue, 31 May 2022 10:37:50 +0800 Subject: [PATCH] add websocket --- server/crm/pom.xml | 8 + .../pudonghot/ambition/crm/AmbitionCRM.java | 17 +- .../support/ExportTaskServiceSupport.java | 8 +- .../crm/ws/config/WebSocketConfig.java | 33 ++ .../crm/ws/service/WebSocketService.java | 21 ++ .../ws/service/impl/WebSocketServiceImpl.java | 25 ++ server/crm/src/main/resources/log4j2_dev.xml | 3 - server/crm/src/main/resources/log4j2_prod.xml | 4 +- server/crm/src/main/resources/log4j2_test.xml | 6 +- .../resources/spring/spring-ambition-crm.xml | 14 - web/app/routes/login.js | 52 +-- web/app/services/ajax.js | 336 +++++++++--------- web/app/services/websocket.js | 42 +++ web/package.json | 115 +++--- 14 files changed, 410 insertions(+), 274 deletions(-) create mode 100644 server/crm/src/main/java/com/pudonghot/ambition/crm/ws/config/WebSocketConfig.java create mode 100644 server/crm/src/main/java/com/pudonghot/ambition/crm/ws/service/WebSocketService.java create mode 100644 server/crm/src/main/java/com/pudonghot/ambition/crm/ws/service/impl/WebSocketServiceImpl.java delete mode 100644 server/crm/src/main/resources/spring/spring-ambition-crm.xml create mode 100644 web/app/services/websocket.js diff --git a/server/crm/pom.xml b/server/crm/pom.xml index 1fac438..09f9ae2 100644 --- a/server/crm/pom.xml +++ b/server/crm/pom.xml @@ -34,6 +34,14 @@ org.springframework.boot spring-boot-starter-log4j2 + + org.springframework + spring-messaging + + + org.springframework + spring-websocket + commons-codec commons-codec diff --git a/server/crm/src/main/java/com/pudonghot/ambition/crm/AmbitionCRM.java b/server/crm/src/main/java/com/pudonghot/ambition/crm/AmbitionCRM.java index dbd7632..1f9fc51 100644 --- a/server/crm/src/main/java/com/pudonghot/ambition/crm/AmbitionCRM.java +++ b/server/crm/src/main/java/com/pudonghot/ambition/crm/AmbitionCRM.java @@ -1,10 +1,14 @@ package com.pudonghot.ambition.crm; +import lombok.val; import org.springframework.boot.SpringApplication; -import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; /** * @author Shaun Chyxion
@@ -12,6 +16,7 @@ import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; * Mar 05, 2017 14:40:30 */ @EnableAsync +@EnableScheduling @EnableAspectJAutoProxy(proxyTargetClass = true) @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class AmbitionCRM { @@ -22,4 +27,14 @@ public class AmbitionCRM { public static void main(String[] args) { SpringApplication.run(AmbitionCRM.class, args); } + + @Bean("exportTaskExecutor") + public ThreadPoolTaskExecutor threadPoolTaskExecutor() { + val threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); + threadPoolTaskExecutor.setCorePoolSize(2); + threadPoolTaskExecutor.setMaxPoolSize(8); + threadPoolTaskExecutor.setKeepAliveSeconds(16); + threadPoolTaskExecutor.setQueueCapacity(64); + return threadPoolTaskExecutor; + } } diff --git a/server/crm/src/main/java/com/pudonghot/ambition/crm/service/support/ExportTaskServiceSupport.java b/server/crm/src/main/java/com/pudonghot/ambition/crm/service/support/ExportTaskServiceSupport.java index 92852b4..65ff52c 100644 --- a/server/crm/src/main/java/com/pudonghot/ambition/crm/service/support/ExportTaskServiceSupport.java +++ b/server/crm/src/main/java/com/pudonghot/ambition/crm/service/support/ExportTaskServiceSupport.java @@ -1,9 +1,10 @@ package com.pudonghot.ambition.crm.service.support; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import org.springframework.core.task.TaskExecutor; import com.pudonghot.ambition.crm.service.ExportTaskService; +import org.springframework.beans.factory.annotation.Autowired; /** * @author Donghuang @@ -12,7 +13,8 @@ import com.pudonghot.ambition.crm.service.ExportTaskService; @Service public class ExportTaskServiceSupport implements ExportTaskService { @Autowired - private ThreadPoolTaskExecutor threadPoolTaskExecutor; + @Qualifier("exportTaskExecutor") + private TaskExecutor threadPoolTaskExecutor; /** * {@inheritDoc} diff --git a/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/config/WebSocketConfig.java b/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/config/WebSocketConfig.java new file mode 100644 index 0000000..f37d6aa --- /dev/null +++ b/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/config/WebSocketConfig.java @@ -0,0 +1,33 @@ +package com.pudonghot.ambition.crm.ws.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +/** + * @author Donghuang + * @date Oct 12, 2021 15:31:50 + */ +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + /** + * {@inheritDoc} + */ + @Override + public void configureMessageBroker(final MessageBrokerRegistry config) { + config.setApplicationDestinationPrefixes("/ws"); + config.enableSimpleBroker("/topic"); + } + + /** + * {@inheritDoc} + */ + @Override + public void registerStompEndpoints(final StompEndpointRegistry registry) { + registry.addEndpoint("/stomp").withSockJS(); + } +} diff --git a/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/service/WebSocketService.java b/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/service/WebSocketService.java new file mode 100644 index 0000000..4a170b9 --- /dev/null +++ b/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/service/WebSocketService.java @@ -0,0 +1,21 @@ +package com.pudonghot.ambition.crm.ws.service; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.NotBlank; +import org.springframework.validation.annotation.Validated; + +/** + * @author Donghuang + * @date May 30, 2022 15:56:24 + */ +@Validated +public interface WebSocketService { + + /** + * publish websocket message + * + * @param employeeKey + * @param payload + */ + void publish(@NotBlank String employeeKey, @NotNull Object payload); +} diff --git a/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/service/impl/WebSocketServiceImpl.java b/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/service/impl/WebSocketServiceImpl.java new file mode 100644 index 0000000..9272f59 --- /dev/null +++ b/server/crm/src/main/java/com/pudonghot/ambition/crm/ws/service/impl/WebSocketServiceImpl.java @@ -0,0 +1,25 @@ +package com.pudonghot.ambition.crm.ws.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.beans.factory.annotation.Autowired; +import com.pudonghot.ambition.crm.ws.service.WebSocketService; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +/** + * @author Donghuang + * @date May 30, 2022 19:07:22 + */ +@Slf4j +@Service +public class WebSocketServiceImpl implements WebSocketService { + + @Autowired + private SimpMessagingTemplate message; + + public void publish(final String employeeKey, + final Object payload) { + log.debug("Publish websocket message [{}] -> [{}].", employeeKey, payload); + message.convertAndSend("/topic/" + employeeKey, payload); + } +} diff --git a/server/crm/src/main/resources/log4j2_dev.xml b/server/crm/src/main/resources/log4j2_dev.xml index 6744678..4cce8d8 100644 --- a/server/crm/src/main/resources/log4j2_dev.xml +++ b/server/crm/src/main/resources/log4j2_dev.xml @@ -21,9 +21,6 @@ - - - diff --git a/server/crm/src/main/resources/log4j2_prod.xml b/server/crm/src/main/resources/log4j2_prod.xml index ef54f63..8d3ff6f 100644 --- a/server/crm/src/main/resources/log4j2_prod.xml +++ b/server/crm/src/main/resources/log4j2_prod.xml @@ -10,8 +10,8 @@ + fileName="${log.dir}/app.log" + filePattern="${log.dir}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd}-%i.log.gz"> diff --git a/server/crm/src/main/resources/log4j2_test.xml b/server/crm/src/main/resources/log4j2_test.xml index a57043b..8d3ff6f 100644 --- a/server/crm/src/main/resources/log4j2_test.xml +++ b/server/crm/src/main/resources/log4j2_test.xml @@ -10,12 +10,12 @@ + fileName="${log.dir}/app.log" + filePattern="${log.dir}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd}-%i.log.gz"> - + diff --git a/server/crm/src/main/resources/spring/spring-ambition-crm.xml b/server/crm/src/main/resources/spring/spring-ambition-crm.xml deleted file mode 100644 index d817648..0000000 --- a/server/crm/src/main/resources/spring/spring-ambition-crm.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - diff --git a/web/app/routes/login.js b/web/app/routes/login.js index e9bc0cc..2bdc275 100644 --- a/web/app/routes/login.js +++ b/web/app/routes/login.js @@ -1,25 +1,27 @@ -import Ember from 'ember'; - -export default Ember.Route.extend({ - activate() { - this.controllerFor('application').set('login', true); - }, - deactivate() { - this.controllerFor('application').set('login', false); - }, - actions: { - doLogin(model) { - let me = this; - me.get('ajax').doPost('auth/login', model, - function(user) { - Ember.Logger.debug(`User ${user} Loggedin`); - me.set('ajax.user', user); - me.message.alert('Sign in successfully'); - // me.transitionTo('/'); - window.location.href = '/'; - }, function(msg) { - me.get('message').warn(msg); - }); - } - } -}); +import Ember from 'ember'; +import $ from 'jquery'; + +export default Ember.Route.extend({ + activate() { + this.controllerFor('application').set('login', true); + }, + deactivate() { + this.controllerFor('application').set('login', false); + }, + actions: { + doLogin(model) { + let me = this; + me.get('ajax').doPost('auth/login', model, + function(user) { + Ember.Logger.debug(`User ${user} Loggedin`); + me.set('ajax.user', user); + me.message.alert('Sign in successfully'); + // me.transitionTo('/'); + window.location.href = '/'; + $.trigger('LOGINED', true); + }, function(msg) { + me.get('message').warn(msg); + }); + } + } +}); diff --git a/web/app/services/ajax.js b/web/app/services/ajax.js index fe0d7db..5707ced 100644 --- a/web/app/services/ajax.js +++ b/web/app/services/ajax.js @@ -1,167 +1,169 @@ -import Ember from 'ember'; - -export default Ember.Service.extend({ - /** - * Ajax.doGet('url.get', - * {id: 001, name: 'foo'}, - * function(data) { - * // success - * }, function(msg) { - * // fail - * }); - * no params - * Ajax.doGet('url.get', - * function(data) { - * // success - * }, function(msg){ - * // fail - * }); - * @param {Boolean} [loadmask], option - * @param {String} url - * @param {Object} [params] - * @param {Function} [fnSucc] - * @param {Function} [fnFail] - */ - doGet() { - this._process_request(arguments, 'GET'); - }, - /** - * @see #get - */ - doPost() { - this._process_request(arguments, 'POST'); - }, - /** - * @see #get - */ - doPut() { - this._process_request(arguments, 'PUT'); - }, - /** - * @see #get - */ - doDel() { - this._process_request(arguments, 'DELETE'); - }, - /** - * private - */ - _request(p) { - var me = this; - p.loadmask && me.set('loading', true); - var formDataCfg; - if (p.params instanceof FormData) { - formDataCfg = false; - } - - Ember.$.ajax({ - url: p.url, - data: p.params, - type: (/get/i).test(p.method) ? 'GET' : 'POST', - contentType: formDataCfg, - processData: formDataCfg, - success(r, status, resp) { - if (r.success) { - // ignore success callback - if (p.fnSucc !== false) { - let keys = Object.keys(r); - me._isFunc(p.fnSucc) ? - p.fnSucc(keys.length === 3 && - keys.includes('data') ? r.data : r) : - me.message.alert(r.data || 'Ajax Request Successfully'); - } - } - else if (r.errcode === 4011) { - Ember.getOwner(me).lookup('route:application').transitionTo('login'); - } - // ignore fail callback - else if (p.fnFail !== false) { - me._isFunc(p.fnFail) ? p.fnFail(r.errmsg, r) : - me.dialog.error(r.errmsg || 'Ajax Request Error Caused'); - } - else { - me.dialog.error(r.errmsg || 'Ajax Request Error Caused'); - } - p.loadmask && me.set('loading', false); - }, - error(jqXHR, textStatus) { - Ember.Logger.error('Ajax Request Error Caused', arguments); - if (textStatus === 'timeout') { - me.dialog.error('Ajax Request Timeout Error Caused.'); - } - else if (textStatus === 'abort') { - me.dialog.error('Ajax Request Client Abort Error Caused.'); - } - else { - me.dialog.error('Ajax Request Error Caused.'); - } - p.loadmask && me.set('loading', false); - } - }); - }, - /** - * { - * [lm], [url], [params], [fnSucc], [fnFail] - * } - */ - _process_request(args, method) { - var me = this; - if (args && args.length) { - // args to array - args = Array.prototype.slice.call(args); - var lm = args.shift(), // loadmask - url, - p, - fnSucc, - fnFail; - // loadmask - if (Ember.$.type(lm) === 'boolean') { - url = args.shift(); - } - else { - url = lm; - // default loadmask - lm = true; - } - me._parse_params_and_callbacks(args); - // params - p = args.shift(); - // callbacks - fnSucc = args.shift(); - fnFail = args.shift(); - // Ajax Request - me._request({ - loadmask: lm, - method: method || 'GET', - url: url, - params: p, - fnSucc: fnSucc, - fnFail: fnFail - }); - } - else { - console.error('Ajax No Args Given.'); - } - }, - /** - * params, fnSucc, fnFail - * fnSucc, fnFail - * fnFail - */ - _parse_params_and_callbacks(args) { - // has callbacks - if (this._isFunc(args[1]) && this._isFunc(args[2])) { - this._isFunc(args[0]) && (args[0] = args[0]()); - } - // no params, params is success callback - else if (this._isFunc(args[0])) { - args[2] = args[1]; - args[1] = args[0]; - args[0] = null; - } - }, - _isFunc(f) { - return Ember.$.type(f) === 'function'; - } -}); - +import Ember from 'ember'; +import $ from 'jquery'; + +export default Ember.Service.extend({ + /** + * Ajax.doGet('url.get', + * {id: 001, name: 'foo'}, + * function(data) { + * // success + * }, function(msg) { + * // fail + * }); + * no params + * Ajax.doGet('url.get', + * function(data) { + * // success + * }, function(msg){ + * // fail + * }); + * @param {Boolean} [loadmask], option + * @param {String} url + * @param {Object} [params] + * @param {Function} [fnSucc] + * @param {Function} [fnFail] + */ + doGet() { + this._process_request(arguments, 'GET'); + }, + /** + * @see #get + */ + doPost() { + this._process_request(arguments, 'POST'); + }, + /** + * @see #get + */ + doPut() { + this._process_request(arguments, 'PUT'); + }, + /** + * @see #get + */ + doDel() { + this._process_request(arguments, 'DELETE'); + }, + /** + * private + */ + _request(p) { + var me = this; + p.loadmask && me.set('loading', true); + var formDataCfg; + if (p.params instanceof FormData) { + formDataCfg = false; + } + + $.ajax({ + url: p.url, + data: p.params, + type: (/get/i).test(p.method) ? 'GET' : 'POST', + contentType: formDataCfg, + processData: formDataCfg, + success(r, status, resp) { + if (r.success) { + // ignore success callback + if (p.fnSucc !== false) { + let keys = Object.keys(r); + me._isFunc(p.fnSucc) ? + p.fnSucc(keys.length === 3 && + keys.includes('data') ? r.data : r) : + me.message.alert(r.data || 'Ajax Request Successfully'); + } + } + else if (r.errcode === 4011) { + $.trigger('NOT_LOGIN', true); + Ember.getOwner(me).lookup('route:application').transitionTo('login'); + } + // ignore fail callback + else if (p.fnFail !== false) { + me._isFunc(p.fnFail) ? p.fnFail(r.errmsg, r) : + me.dialog.error(r.errmsg || 'Ajax Request Error Caused'); + } + else { + me.dialog.error(r.errmsg || 'Ajax Request Error Caused'); + } + p.loadmask && me.set('loading', false); + }, + error(jqXHR, textStatus) { + Ember.Logger.error('Ajax Request Error Caused', arguments); + if (textStatus === 'timeout') { + me.dialog.error('Ajax Request Timeout Error Caused.'); + } + else if (textStatus === 'abort') { + me.dialog.error('Ajax Request Client Abort Error Caused.'); + } + else { + me.dialog.error('Ajax Request Error Caused.'); + } + p.loadmask && me.set('loading', false); + } + }); + }, + /** + * { + * [lm], [url], [params], [fnSucc], [fnFail] + * } + */ + _process_request(args, method) { + var me = this; + if (args && args.length) { + // args to array + args = Array.prototype.slice.call(args); + var lm = args.shift(), // loadmask + url, + p, + fnSucc, + fnFail; + // loadmask + if ($.type(lm) === 'boolean') { + url = args.shift(); + } + else { + url = lm; + // default loadmask + lm = true; + } + me._parse_params_and_callbacks(args); + // params + p = args.shift(); + // callbacks + fnSucc = args.shift(); + fnFail = args.shift(); + // Ajax Request + me._request({ + loadmask: lm, + method: method || 'GET', + url: url, + params: p, + fnSucc: fnSucc, + fnFail: fnFail + }); + } + else { + console.error('Ajax No Args Given.'); + } + }, + /** + * params, fnSucc, fnFail + * fnSucc, fnFail + * fnFail + */ + _parse_params_and_callbacks(args) { + // has callbacks + if (this._isFunc(args[1]) && this._isFunc(args[2])) { + this._isFunc(args[0]) && (args[0] = args[0]()); + } + // no params, params is success callback + else if (this._isFunc(args[0])) { + args[2] = args[1]; + args[1] = args[0]; + args[0] = null; + } + }, + _isFunc(f) { + return $.type(f) === 'function'; + } +}); + diff --git a/web/app/services/websocket.js b/web/app/services/websocket.js new file mode 100644 index 0000000..1120e50 --- /dev/null +++ b/web/app/services/websocket.js @@ -0,0 +1,42 @@ +import Service from '@ember/service'; +import { aliasMethod } from '@ember/object'; +import $ from 'jquery'; + +export default Service.extend({ + connect() { + let me = this; + if (me.get('connected')) { + Ember.Logger.info('Websocket is connected, reconnect.'); + me.disconnect(); + } + + // connect + if (!me.get('stompClient')) { + me.set('stompClient', Stomp.over(new SockJS('/stomp'))); + } + me.get('stompClient').connect({}, function() { + Ember.Logger.info('Connect websocket.'); + me.stompClient.subscribe('/topic/websocket', function(msg) { + Ember.Logger.info('On websocket message: ', msg); + $.trigger('websocket', JSON.parse(msg.body)); + }); + me.set('connected', true); + Ember.Logger.info('Websocket connected.'); + }); + }, + willDestroy() { + var me = this; + me._super(...arguments); + + if (me.get('connected') && me.get('stompClient')) { + me.get('stompClient').disconnect(); + me.set('connected', false); + Ember.Logger.info('Websocket disconnected.'); + } + else { + Ember.Logger.info('Websocket is not connected, ignore disconnect.'); + } + }, + disconnect: aliasMethod('willDestroy') +}); + diff --git a/web/package.json b/web/package.json index cf61fb0..c82220c 100644 --- a/web/package.json +++ b/web/package.json @@ -1,56 +1,59 @@ -{ - "name": "ambition-crm", - "version": "0.0.1", - "description": "Ambition CRM", - "private": true, - "directories": { - "doc": "doc", - "test": "tests" - }, - "scripts": { - "build": "ember build", - "start": "ember server", - "test": "ember test" - }, - "repository": "", - "author": "Shaun Chyxion", - "license": "MIT", - "devDependencies": { - "broccoli-asset-rev": "^2.4.5", - "broccoli-funnel": "^1.0.1", - "ember-ajax": "^3.0.0", - "ember-browserify": "^1.1.11", - "ember-cli": "~2.14.2", - "ember-cli-app-version": "^3.0.0", - "ember-cli-babel": "^6.3.0", - "ember-cli-dependency-checker": "^1.3.0", - "ember-cli-eslint": "^3.0.0", - "ember-cli-htmlbars": "^2.0.1", - "ember-cli-htmlbars-inline-precompile": "^0.4.3", - "ember-cli-inject-live-reload": "^1.4.1", - "ember-cli-jshint": "^1.0.0", - "ember-cli-less": "^1.5.5", - "ember-cli-moment-shim": "^1.2.0", - "ember-cli-qunit": "^4.0.0", - "ember-cli-release": "0.2.8", - "ember-cli-shims": "^1.1.0", - "ember-cli-sri": "^2.1.0", - "ember-cli-uglify": "^1.2.0", - "ember-export-application-global": "^2.0.0", - "ember-load-initializers": "^1.0.0", - "ember-moment": "^6.1.0", - "ember-radio-button": "1.1.1", - "ember-resolver": "^4.0.0", - "ember-route-action-helper": "2.0.3", - "ember-source": "~2.16.0", - "ember-truth-helpers": "1.1.0", - "emberx-select": "3.0.1", - "glob": "^4.5.3", - "loader.js": "^4.2.3", - "morgan": "^1.7.0" - }, - "dependencies": {}, - "engines": { - "node": "^4.5 || 6.* || >= 7.*" - } -} +{ + "name": "ambition-crm", + "version": "0.0.1", + "description": "Ambition CRM", + "private": true, + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "build": "ember build", + "start": "ember server", + "test": "ember test" + }, + "repository": "", + "author": "Shaun Chyxion", + "license": "MIT", + "devDependencies": { + "broccoli-asset-rev": "^2.4.5", + "broccoli-funnel": "^1.0.1", + "ember-ajax": "^3.0.0", + "ember-browserify": "^1.1.11", + "ember-cli": "~2.14.2", + "ember-cli-app-version": "^3.0.0", + "ember-cli-babel": "^6.3.0", + "ember-cli-dependency-checker": "^1.3.0", + "ember-cli-eslint": "^3.0.0", + "ember-cli-htmlbars": "^2.0.1", + "ember-cli-htmlbars-inline-precompile": "^0.4.3", + "ember-cli-inject-live-reload": "^1.4.1", + "ember-cli-jshint": "^1.0.0", + "ember-cli-less": "^1.5.5", + "ember-cli-moment-shim": "^1.2.0", + "ember-cli-qunit": "^4.0.0", + "ember-cli-release": "0.2.8", + "ember-cli-shims": "^1.1.0", + "ember-cli-sri": "^2.1.0", + "ember-cli-uglify": "^1.2.0", + "ember-export-application-global": "^2.0.0", + "ember-load-initializers": "^1.0.0", + "ember-moment": "^6.1.0", + "ember-radio-button": "1.1.1", + "ember-resolver": "^4.0.0", + "ember-route-action-helper": "2.0.3", + "ember-source": "~2.16.0", + "ember-truth-helpers": "1.1.0", + "emberx-select": "3.0.1", + "glob": "^4.5.3", + "loader.js": "^4.2.3", + "morgan": "^1.7.0" + }, + "dependencies": { + "sockjs-client": "^1.6.1", + "stomp-websocket": "^2.3.4-next" + }, + "engines": { + "node": "^4.5 || 6.* || >= 7.*" + } +}