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.*"
+ }
+}