TT#23601 Call: As a Developer I want to have the cdk (RTC:engine) integrated in the project

- Make cdk available as js module
- Provide a store that reflects all events of a call
- Integrate the call globally in the vue.js stack
- Register on all global events (client.onConnected, client.onDisconnected, network.onConnect, network.onDisconnect) to get the connection state with RTC:engine

Change-Id: I40b61dd4c82ef6bd0974b693328740ab088df5a4
changes/69/16369/2
Hans-Peter Herzog 8 years ago
parent 24d73971f8
commit 15729c0144

970
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

@ -1,7 +1,7 @@
{
"name": "ngcp-csc-ui",
"productName": "Customer Self-Care Web UI",
"version": "0.0.6",
"version": "0.1.1",
"description": "Customer Self-Care Web UI",
"author": "Hans-Peter Herzog <hherzog@sipwise.com>",
"scripts": {
@ -20,9 +20,9 @@
"html-entities": "^1.2.1",
"lodash": "^4.17.4",
"quasar-extras": "0.x",
"quasar-framework": "^0.14.4",
"quasar-framework": "0.14.4",
"strip-ansi": "^4.0.0",
"vue": "~2.3.4",
"vue": "2.3.4",
"vue-i18n": "^7.3.0",
"vue-resource": "^1.3.4",
"vue-router": "^2.7.0",
@ -66,6 +66,7 @@
"karma-junit-reporter": "^1.2.0",
"karma-mocha": "^1.3.0",
"karma-webpack": "^2.0.4",
"load-script": "1.0.0",
"mocha": "^4.0.0",
"opn": "^5.0.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",

@ -0,0 +1,47 @@
import Vue from 'vue';
import { getJsonBody } from './utils';
export function create() {
return new Promise((resolve, reject)=>{
Vue.http.post('/api/rtcsessions/').then((res)=>{
resolve(res);
}).catch((err)=>{
reject(err);
});
});
}
export function getByUrl(url) {
return new Promise((resolve, reject)=>{
Vue.http.get(url).then((res)=>{
resolve(getJsonBody(res.body));
}).catch((err)=>{
reject(err);
});
});
}
export function createSession() {
return new Promise((resolve, reject)=>{
Promise.resolve().then(()=>{
return create();
}).then((res)=>{
return getByUrl(res.headers.get('Location'));
}).then((res)=>{
resolve(res);
}).catch((err)=>{
reject(err);
});
});
}
export function createSessionToken() {
return new Promise((resolve, reject)=>{
createSession().then((res)=>{
resolve(res.rtc_browser_token);
}).catch((err)=>{
reject(err);
});
});
}

@ -69,8 +69,7 @@ export function getCapabilities() {
export function getNumbers() {
return new Promise((resolve, reject)=>{
Vue.http.get('/api/numbers').then((result)=>{
// Todo: Check format of numbers
resolve();
resolve(result);
}).catch((err)=>{
reject(err);
});

@ -28,6 +28,7 @@
<style lang="stylus">
@import '../../src/themes/app.variables.styl';
@import '../../src/themes/quasar.variables.styl';
.page {
padding: 60px;
padding-top: 100px;
@ -44,10 +45,30 @@
right: 0;
padding: 30px;
padding-left: 60px;
padding-right: 60px;
background-color: white;
z-index: 1000;
}
@media (max-width: $breakpoint-sm) {
.page {
padding: 30px;
padding-top: 100px;
}
.page h2 {
margin: 0px;
font-size: 22px;
line-height: 22px;
}
.page .page-title {
right: 0;
padding: 30px;
background-color: white;
z-index: 1000;
}
}
.page-title-icon {
margin-right: 10px;
font-size: 24px !important;

@ -82,26 +82,28 @@
</q-side-link>
</q-collapsible>
</q-list>
<q-fixed-position corner="top-right" :offset="[60, 16]" class="page-action-button">
<q-fab color="primary" icon="question answer" active-icon="clear" direction="left" flat>
<q-fab-action color="primary" @click="" icon="fa-fax" flat>
<q-tooltip anchor="bottom middle" self="top middle" :offset="[0, 15]">{{ $t('sendFax') }}</q-tooltip>
<div id="page-action-button">
<q-fab v-if="hasCommunicationCapabilities"
color="primary" icon="question answer"
active-icon="clear" direction="down" flat>
<q-fab-action v-if="hasFaxCapability" color="primary" @click="" icon="fa-fax">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendFax') }}</q-tooltip>
</q-fab-action>
<q-fab-action color="primary" @click="" icon="fa-send" flat>
<q-tooltip anchor="bottom middle" self="top middle" :offset="[0, 15]">{{ $t('sendSms') }}</q-tooltip>
<q-fab-action v-if="hasSmsCapability" color="primary" @click="" icon="fa-send">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('sendSms') }}</q-tooltip>
</q-fab-action>
<q-fab-action v-bind:color="(rtcEngineConnected)?'primary':'light'" @click="startCall" icon="fa-phone" flat>
<q-tooltip anchor="bottom middle" self="top middle" :offset="[0, 15]">{{ $t('startCall') }}</q-tooltip>
<q-fab-action v-if="isCallAvailable" color="primary" @click="" icon="fa-phone">
<q-tooltip anchor="center right" self="center left" :offset="[15, 0]">{{ $t('startCall') }}</q-tooltip>
</q-fab-action>
</q-fab>
</q-fixed-position>
</div>
<router-view />
</q-layout>
</template>
<script>
import _ from 'lodash';
import { startLoading, stopLoading, showGlobalError } from '../../helpers/ui'
import { startLoading, stopLoading, showGlobalError, showToast } from '../../helpers/ui'
import { mapState, mapGetters } from 'vuex'
import {
QLayout,
@ -127,13 +129,16 @@
name: 'default',
mounted: function() {
this.$refs.layout.showLeft();
if(!this.$store.getters['user/hasUser']) {
if(!this.hasUser) {
startLoading();
this.$store.dispatch('user/initUser').then(()=>{
stopLoading();
this.showInitialToasts();
}).catch(()=>{
this.logout();
});
} else {
this.showInitialToasts();
}
},
components: {
@ -157,32 +162,63 @@
QCollapsible
},
computed: {
...mapGetters('user', ['getUsername', 'isPbxAdmin']),
...mapGetters('call', [
'isCallAvailable',
'hasCallInitFailure'
]),
...mapGetters('user', [
'hasUser',
'getUsername',
'isPbxAdmin',
'hasSmsCapability',
'hasFaxCapability'
]),
...mapState({
rtcEngineConnected: state => state.rtcEngineConnected,
isCallForward: state => _.startsWith(state.route.path, '/user/call-forward'),
isCallBlocking: state => _.startsWith(state.route.path, '/user/call-blocking'),
isPbxConfiguration: state => _.startsWith(state.route.path, '/user/pbx-configuration')
})
}),
hasCommunicationCapabilities() {
return this.isCallAvailable || this.hasSmsCapability || this.hasFaxCapability;
}
},
methods: {
showInitialToasts() {
if(this.isCallAvailable) {
showToast(this.$i18n.t('toasts.callIsAvailable'));
}
if(this.hasCallInitFailure) {
showToast(this.$i18n.t('toasts.callIsNotAvailable'));
}
},
logout() {
startLoading();
this.$store.dispatch('user/logout').then(()=>{
stopLoading();
this.$router.push({path: '/login'});
})
},
startCall() {
if(!this.$store.state.rtcEngineConnected) {
showGlobalError(this.$t('rtcEngineDisconnected'));
}
}
}
}
</script>
<style>
<style lang="stylus">
@import '../../../src/themes/app.variables.styl';
@import '../../../src/themes/quasar.variables.styl';
#page-action-button {
z-index: 1001;
position: fixed;
top: ($toolbar-min-height + 15)px;
right: 55px;
}
@media (max-width: $breakpoint-sm) {
#page-action-button {
right: 25px;
}
}
#main-menu {
padding-top:60px;
}
@ -240,8 +276,4 @@
padding: 15px;
margin: 0;
}
.page-action-button {
z-index: 1001;
}
</style>

@ -0,0 +1,68 @@
import loadScript from 'load-script'
var scriptId = 'cdk';
var scriptPath = '/rtc/files/dist/cdk-prod.js';
var webSocketPath = '/rtc/api';
var webSocketUrl = 'wss://' + window.location.host + webSocketPath;
export function loadCdkLib() {
return new Promise((resolve, reject)=>{
if(!document.getElementById(scriptId)) {
loadScript(scriptPath, {
attrs: {
id: scriptId
}
}, function(err, script){
if(err) {
reject(err);
} else {
resolve(script);
}
});
} else {
resolve();
}
});
}
export function connectCdkClient(session) {
return new Promise((resolve, reject)=>{
var client = new cdk.Client({
url: webSocketUrl,
userSession: session
});
client.onConnect(()=>{
resolve(client);
});
client.onDisconnect(()=>{
reject(new Error(client.disconnectReason));
});
});
}
export function connectCdkNetwork(session, networkTag) {
return new Promise((resolve, reject)=>{
Promise.resolve().then(()=>{
return connectCdkClient(session);
}).then((client)=>{
return new Promise(($resolve, $reject)=>{
var network = client.getNetworkByTag(networkTag);
network.onConnect(()=>{
$resolve(network);
});
network.onDisconnect(()=>{
$reject(new Error(network.disconnectReason));
});
});
}).then((network)=>{
resolve(network);
}).catch((err)=>{
reject(err);
});
});
}
export function connectDefaultCdkNetwork(session) {
return connectCdkNetwork(session, 'sip');
}

@ -87,5 +87,9 @@
"timeUpdatedMsg": "Time updated!",
"recurrenceUpdatedMsg": "Recurrence updated!"
}
},
"toasts": {
"callIsAvailable:": "You are now able to start and receive calls",
"callIsNotAvailable": "Could not initialize call functionality properly"
}
}

@ -0,0 +1,98 @@
'use strict';
import { loadCdkLib, connectDefaultCdkNetwork } from '../helpers/cdk-lib';
import { createSessionToken } from '../api/rtcsession';
var cdkNetwork = null;
export default {
namespaced: true,
state: {
loaded: false,
initFailure: false,
connected: false,
disconnectReason: '',
incoming: false,
incomingNumber: '',
outgoing: false,
outgoingNumber: ''
},
getters: {
isCallAvailable(state, getters) {
return state.loaded && state.connected;
},
hasCallInitFailure(state, getters) {
return state.initFailure;
}
},
mutations: {
load(state) {
state.loaded = true;
},
initFailure(state) {
state.initFailure = true;
},
connect(state) {
state.connected = true;
},
disconnect(state, reason) {
state.connected = false;
state.disconnectReason = reason;
},
incoming(state) {
},
outgoing(state) {
}
},
actions: {
initialize(context) {
return new Promise((resolve, reject)=>{
if(context.rootState.user.capabilities.rtcengine) {
loadCdkLib().then((script)=>{
context.commit('load');
return createSessionToken();
}).then((sessionToken)=>{
return connectDefaultCdkNetwork(sessionToken);
}).then(($cdkNetwork)=>{
cdkNetwork = $cdkNetwork;
cdkNetwork.getClient().onConnect(()=>{
context.commit('connect');
});
cdkNetwork.getClient().onDisconnect(()=>{
context.commit('disconnect', cdkNetwork.disconnectReason);
});
context.commit('connect');
resolve();
}).catch((err)=>{
context.commit('initFailure');
resolve();
});
} else {
resolve();
}
});
},
call(context) {
},
connect(context, sessionToken) {
},
disconnect(context) {
},
enableAudio(context) {
},
disableAudio(context) {
},
enableVideo(context) {
},
disableVideo(context) {
}
}
};

@ -1,6 +1,5 @@
'use strict';
import _ from 'lodash'
import Vue from 'vue'
import Vuex from 'vuex'
@ -8,9 +7,7 @@ import UserModule from './user'
import PbxGroupsModule from './pbx-groups'
import CallBlockingModule from './call-blocking'
import ReminderModule from './reminder'
var rtcEngineClient = null;
var rtcEngineNetwork = null;
import CallModule from './call'
Vue.use(Vuex);
@ -19,94 +16,7 @@ export const store = new Vuex.Store({
user: UserModule,
pbxGroups: PbxGroupsModule,
callBlocking: CallBlockingModule,
reminder: ReminderModule
},
state: {
rtcEngineConnected: false
},
getters: {},
mutations: {
disconnectRtcEngine(state) {
state.rtcEngineConnected = false;
},
connectRtcEngine(state) {
state.rtcEngineConnected = true;
}
},
actions: {
createRtcEngineSession(context) {
return new Promise((resolve, reject)=>{
Promise.resolve().then(()=>{
return Vue.http.post('/api/rtcsessions/');
}).then((res)=>{
return Vue.http.get(res.headers.get('Location'));
}).then((res)=>{
return res.json();
}).then((body)=>{
localStorage.setItem('rtcEngineSession', body.rtc_browser_token);
resolve(localStorage.getItem('rtcEngineSession'));
}).catch((err)=>{
reject(err);
});
});
},
connectRtcEngine(context, options) {
return new Promise((resolve, reject)=>{
var force = _.get(options, 'force', false);
var isConnected = rtcEngineClient instanceof cdk.Client && _.isEmpty(rtcEngineClient.disconnectReason);
if(isConnected && !force) {
resolve();
} else {
Promise.resolve().then(()=>{
return context.dispatch('disconnectRtcEngine');
}).then(()=>{
return context.dispatch('createRtcEngineSession');
}).then((sessionToken)=>{
rtcEngineClient = new cdk.Client({
url: 'wss://' + window.location.host + '/rtc/api',
userSession: sessionToken
});
rtcEngineClient.onConnect(()=>{
rtcEngineNetwork = rtcEngineClient.getNetworkByTag('sip');
rtcEngineNetwork.onConnect(()=>{
context.commit('connectRtcEngine');
resolve();
});
rtcEngineNetwork.onDisconnect(()=>{
context.commit('disconnectRtcEngine');
reject(new Error('NetworkError: ' + rtcEngineNetwork.disconnectReason));
});
});
rtcEngineClient.onDisconnect(()=>{
context.commit('disconnectRtcEngine');
reject(new Error('ClientError: ' + rtcEngineClient.disconnectReason));
});
}).catch((err)=>{
context.commit('disconnectRtcEngine');
reject(err);
});
}
});
},
disconnectRtcEngine(context) {
return new Promise((resolve, reject)=>{
context.commit('disconnectRtcEngine');
localStorage.removeItem('rtcEngineSession');
if(rtcEngineClient instanceof cdk.Client && _.isEmpty(rtcEngineClient.disconnectReason)) {
rtcEngineClient.onDisconnect(()=>{
rtcEngineClient = null;
rtcEngineNetwork = null;
resolve();
});
rtcEngineClient.disconnect();
} else {
rtcEngineClient = null;
rtcEngineNetwork = null;
resolve();
}
});
}
reminder: ReminderModule,
call: CallModule
}
});

@ -1,8 +1,7 @@
'use strict';
import _ from 'lodash';
import { login, getCapabilities, getUserData} from '../api/user';
import { login, getUserData} from '../api/user';
export default {
namespaced: true,
@ -33,14 +32,18 @@ export default {
},
isPbxAdmin(state, getters) {
return getters.isAdmin && state.capabilities !== null && state.capabilities.cloudpbx;
}
},
hasSmsCapability(state, getters) {
return state.capabilities !== null && state.capabilities.sms;
},
hasFaxCapability(state, getters) {
return state.capabilities !== null && state.capabilities.faxserver;
},
},
mutations: {
login(state, options) {
state.jwt = options.jwt;
state.subscriberId = options.subscriberId;
state.subscriber = options.subscriber;
state.capabilities = options.capabilities;
},
setUserData(state, options) {
state.subscriber = options.subscriber;
@ -59,15 +62,13 @@ export default {
login(options.username, options.password).then((result)=>{
localStorage.setItem('jwt', result.jwt);
localStorage.setItem('subscriberId', result.subscriberId);
}).then(()=>{
return getUserData(localStorage.getItem('subscriberId'));
}).then((result)=>{
context.commit('login', {
jwt: localStorage.getItem('jwt'),
subscriberId: localStorage.getItem('subscriberId'),
subscriber: result.subscriber,
capabilities: result.capabilities
});
}).then(()=>{
return context.dispatch('initUser');
}).then(()=>{
resolve();
}).catch((err)=>{
reject(err);
@ -78,9 +79,6 @@ export default {
return new Promise((resolve, reject)=>{
localStorage.removeItem('jwt');
localStorage.removeItem('subscriberId');
context.dispatch('disconnectRtcEngine', null, {root: true}).then(()=>{
context.commit('disconnectRtcEngine');
});
context.commit('logout');
resolve();
});
@ -92,6 +90,8 @@ export default {
subscriber: result.subscriber,
capabilities: result.capabilities
});
return context.dispatch('call/initialize', null, { root: true });
}).then(()=>{
resolve();
}).catch((err)=>{
reject(err);

@ -36,3 +36,5 @@ $layout-aside-left-width = 260px
$layout-aside-background = #32404E
$layout-footer-shadow = $no-shadow
$tooltip-background = $primary

Loading…
Cancel
Save