TT#23018 Setup: Create and prepare project skeleton for new CSC Web UI

- Initial vue + vuex + vue-router + webpack + quasar setup
- Added karma test runner
- Added shrinkwrap file
- Added junit reporter
- Fetch subscriber
- Fetch capabilities
- Added Dockerfile + testrunner
- Added development script for ngcp

Change-Id: Ia9e58412d2b71c7e541f3d764f8fda747522c7e6
changes/00/16000/14
Hans-Peter Herzog 8 years ago
parent 402d7ea6e1
commit 99bd2371fb

@ -0,0 +1,5 @@
{
"presets": [["es2015", {"modules": false}], "stage-2"],
"plugins": ["transform-runtime"],
"comments": false
}

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[package.json]
indent_size = 2

@ -0,0 +1,3 @@
build/*.js
config/*.js
dist/*.js

@ -0,0 +1,39 @@
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true
},
// https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
extends: [
'standard'
],
// required to lint *.vue files
plugins: [
'html',
'import'
],
globals: {
'cordova': true,
'DEV': true,
'PROD': true,
'__THEME': true
},
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
'one-var': 0,
'import/first': 0,
'import/named': 2,
'import/namespace': 2,
'import/default': 2,
'import/export': 2,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'brace-style': [2, 'stroustrup', { 'allowSingleLine': true }]
}
}

16
.gitignore vendored

@ -0,0 +1,16 @@
.DS_Store
.thumbs.db
node_modules/
dist/
npm-debug.log*
cordova/platforms
cordova/plugins
# Junit reports
t/TESTS*
# IntelliJ
.idea
*.iml
csc/

@ -0,0 +1,2 @@
save-prefix =
registry = https://npm-registry.sipwise.com/

@ -0,0 +1,35 @@
{
"blocks": "never",
"brackets": "never",
"colons": "never",
"colors": "always",
"commaSpace": "always",
"commentSpace": "always",
"cssLiteral": "never",
"depthLimit": false,
"duplicates": true,
"efficient": "always",
"extendPref": false,
"globalDupe": true,
"indentPref": 2,
"leadingZero": "never",
"maxErrors": false,
"maxWarnings": false,
"mixed": false,
"namingConvention": false,
"namingConventionStrict": false,
"none": "never",
"noImportant": false,
"parenSpace": "never",
"placeholder": false,
"prefixVarsWithDollar": "always",
"quotePref": "single",
"semicolons": "never",
"sortOrder": false,
"stackedProperties": "never",
"trailingWhitespace": "never",
"universal": "never",
"valid": true,
"zeroUnits": "never",
"zIndexNormalize": false
}

@ -0,0 +1,4 @@
# Customer Self-Care Web UI
## Development

@ -0,0 +1,91 @@
var
ExtractTextPlugin = require('extract-text-webpack-plugin'),
autoprefixer = require('autoprefixer'),
purify = require('purify-css'),
glob = require('glob'),
path = require('path'),
fs = require('fs')
module.exports.postcss = [autoprefixer()]
module.exports.styleLoaders = function (options) {
options = options || {}
function generateLoaders (loaders) {
if (options.postcss) {
loaders.splice(1, 0, 'postcss')
}
var sourceLoader = loaders.map(function (loader) {
var extraParamChar
if (/\?/.test(loader)) {
loader = loader.replace(/\?/, '-loader?')
extraParamChar = '&'
}
else {
loader = loader + '-loader'
extraParamChar = '?'
}
return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '')
}).join('!')
if (options.extract) {
return ExtractTextPlugin.extract({
use: sourceLoader,
fallback: 'vue-style-loader'
})
}
else {
return ['vue-style-loader', sourceLoader].join('!')
}
}
return {
css: generateLoaders(['css']),
less: generateLoaders(['css', 'less']),
sass: generateLoaders(['css', 'sass?indentedSyntax']),
scss: generateLoaders(['css', 'sass']),
styl: generateLoaders(['css', 'stylus']),
stylus: generateLoaders(['css', 'stylus'])
}
}
module.exports.styleRules = function (options) {
var output = []
var loaders = exports.styleLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
loader: loader
})
}
return output
}
function getSize (size) {
return (size / 1024).toFixed(2) + 'kb'
}
module.exports.purify = function(cb) {
var css = glob.sync(path.join(__dirname, '../dist/**/*.css'))
var js = glob.sync(path.join(__dirname, '../dist/**/*.js'))
Promise.all(css.map(function (file) {
return new Promise(function (resolve) {
console.log('\n Purifying ' + path.relative(path.join(__dirname, '../dist'), file).bold + '...')
purify(js, [file], {minify: true}, function (purified) {
var oldSize = fs.statSync(file).size
fs.writeFileSync(file, purified)
var newSize = fs.statSync(file).size
console.log(
' * Reduced size by ' + ((1 - newSize / oldSize) * 100).toFixed(2) + '%, from ' +
getSize(oldSize) + ' to ' + getSize(newSize) + '.'
)
resolve()
})
})
}))
.then(cb)
}

@ -0,0 +1,13 @@
var
config = require('../config'),
theme = process.argv[2] || config.defaultTheme
module.exports = {
dev: process.env.NODE_ENV === 'development',
prod: process.env.NODE_ENV === 'production',
platform: {
theme: theme,
cordovaAssets: './cordova/platforms/' + (theme === 'mat' ? 'android' : 'ios') + '/platform_www'
}
}

@ -0,0 +1,3 @@
/* eslint-disable */
require('eventsource-polyfill')
require('webpack-hot-middleware/client?noInfo=true&reload=true')

@ -0,0 +1,57 @@
process.env.NODE_ENV = 'production'
require('colors')
var
shell = require('shelljs'),
path = require('path'),
env = require('./env-utils'),
css = require('./css-utils'),
config = require('../config'),
webpack = require('webpack'),
webpackConfig = require('./webpack.prod.conf'),
targetPath = path.join(__dirname, '../dist')
console.log(' WARNING!'.bold)
console.log(' Do NOT use VueRouter\'s "history" mode if')
console.log(' building for Cordova or Electron.\n')
require('./script.clean.js')
console.log((' Building Quasar App with "' + env.platform.theme + '" theme...\n').bold)
shell.mkdir('-p', targetPath)
shell.cp('-R', 'src/statics', targetPath)
function finalize () {
console.log((
'\n Build complete with "' + env.platform.theme.bold + '" theme in ' +
'"/dist"'.bold + ' folder.\n').cyan)
console.log(' Built files are meant to be served over an HTTP server.'.bold)
console.log(' Opening index.html over file:// won\'t work.'.bold)
}
webpackConfig.watch = (process.env['CSC_WATCH'])? true : false;
webpack(webpackConfig, function (err, stats) {
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n')
if (stats.hasErrors()) {
process.exit(1)
}
if (config.build.purifyCSS) {
css.purify(finalize)
}
else {
finalize()
}
})

@ -0,0 +1,7 @@
var
shell = require('shelljs'),
path = require('path')
shell.rm('-rf', path.resolve(__dirname, '../dist/*'))
shell.rm('-rf', path.resolve(__dirname, '../dist/.*'))
console.log(' Cleaned build artifacts.\n')

@ -0,0 +1,86 @@
process.env.NODE_ENV = 'development'
require('colors')
var
path = require('path'),
express = require('express'),
webpack = require('webpack'),
env = require('./env-utils'),
config = require('../config'),
opn = require('opn'),
proxyMiddleware = require('http-proxy-middleware'),
webpackConfig = require('./webpack.dev.conf'),
app = express(),
port = process.env.PORT || config.dev.port,
uri = 'http://localhost:' + port
console.log(' Starting dev server with "' + (process.argv[2] || env.platform.theme).bold + '" theme...')
console.log(' Will listen at ' + uri.bold)
if (config.dev.openBrowser) {
console.log(' Browser will open when build is ready.\n')
}
var compiler = webpack(webpackConfig)
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
quiet: true
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: function () {}
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
// proxy requests like API. See /config/index.js -> dev.proxyTable
// https://github.com/chimurai/http-proxy-middleware
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(context, options))
})
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())
// serve webpack bundle output
app.use(devMiddleware)
// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)
// serve pure static assets
var staticsPath = path.posix.join(webpackConfig.output.publicPath, 'statics/')
app.use(staticsPath, express.static('./src/statics'))
// try to serve Cordova statics for Play App
app.use(express.static(env.platform.cordovaAssets))
module.exports = app.listen(port, function (err) {
if (err) {
console.log(err)
process.exit(1)
}
// open browser if set so in /config/index.js
if (config.dev.openBrowser) {
devMiddleware.waitUntilValid(function () {
opn(uri)
})
}
})

@ -0,0 +1,108 @@
var
path = require('path'),
webpack = require('webpack'),
config = require('../config'),
cssUtils = require('./css-utils'),
env = require('./env-utils'),
merge = require('webpack-merge'),
projectRoot = path.resolve(__dirname, '../'),
ProgressBarPlugin = require('progress-bar-webpack-plugin'),
useCssSourceMap =
(env.dev && config.dev.cssSourceMap) ||
(env.prod && config.build.productionSourceMap)
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: config[env.prod ? 'build' : 'dev'].publicPath,
filename: 'js/[name].js',
chunkFilename: 'js/[id].[chunkhash].js'
},
resolve: {
extensions: ['.js', '.vue', '.json'],
modules: [
resolve('src'),
resolve('node_modules')
],
alias: config.aliases
},
module: {
rules: [
// { // eslint
// enforce: 'pre',
// test: /\.(vue|js)$/,
// loader: 'eslint-loader',
// include: projectRoot,
// exclude: /node_modules/,
// options: {
// formatter: require('eslint-friendly-formatter')
// }
// },
{
test: /\.js$/,
loader: 'babel-loader',
include: projectRoot,
exclude: /node_modules/
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
postcss: cssUtils.postcss,
loaders: merge({js: 'babel-loader'}, cssUtils.styleLoaders({
sourceMap: useCssSourceMap,
extract: env.prod
}))
}
},
{
test: /\.json$/,
loader: 'json-loader'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'img/[name].[hash:7].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]'
}
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env': config[env.prod ? 'build' : 'dev'].env,
'DEV': env.dev,
'PROD': env.prod,
'__THEME': '"' + env.platform.theme + '"'
}),
new webpack.LoaderOptionsPlugin({
minimize: env.prod,
options: {
context: path.resolve(__dirname, '../src'),
postcss: cssUtils.postcss
}
}),
new ProgressBarPlugin({
format: config.progressFormat
})
],
performance: {
hints: false
}
}

@ -0,0 +1,43 @@
var
config = require('../config'),
webpack = require('webpack'),
merge = require('webpack-merge'),
cssUtils = require('./css-utils'),
baseWebpackConfig = require('./webpack.base.conf'),
HtmlWebpackPlugin = require('html-webpack-plugin'),
FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/hot-reload.js', baseWebpackConfig.entry[name]]
})
module.exports = merge(baseWebpackConfig, {
// eval-source-map is faster for development
devtool: '#cheap-module-eval-source-map',
devServer: {
historyApiFallback: true,
noInfo: true
},
module: {
rules: cssUtils.styleRules({
sourceMap: config.dev.cssSourceMap,
postcss: true
})
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'src/index.html',
inject: true
}),
new FriendlyErrorsPlugin({
clearConsole: config.dev.clearConsoleOnRebuild
})
],
performance: {
hints: false
}
})

@ -0,0 +1,78 @@
var
path = require('path'),
config = require('../config'),
cssUtils = require('./css-utils'),
webpack = require('webpack'),
merge = require('webpack-merge'),
baseWebpackConfig = require('./webpack.base.conf'),
ExtractTextPlugin = require('extract-text-webpack-plugin'),
HtmlWebpackPlugin = require('html-webpack-plugin'),
OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = merge(baseWebpackConfig, {
module: {
rules: cssUtils.styleRules({
sourceMap: config.build.productionSourceMap,
extract: true,
postcss: true
})
},
devtool: config.build.productionSourceMap ? '#source-map' : false,
plugins: [
new webpack.optimize.UglifyJsPlugin({
sourceMap: config.build.productionSourceMap,
minimize: true,
compress: {
warnings: false
}
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
// extract css into its own file
new ExtractTextPlugin({
filename: '[name].[contenthash].css'
}),
new HtmlWebpackPlugin({
filename: path.resolve(__dirname, '../dist/index.html'),
template: 'src/index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
(
module.resource.indexOf('quasar') > -1 ||
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
})
]
})

@ -0,0 +1,6 @@
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

@ -0,0 +1,64 @@
var path = require('path')
module.exports = {
// Webpack aliases
aliases: {
quasar: path.resolve(__dirname, '../node_modules/quasar-framework/'),
src: path.resolve(__dirname, '../src'),
assets: path.resolve(__dirname, '../src/assets'),
'@': path.resolve(__dirname, '../src/components'),
variables: path.resolve(__dirname, '../src/themes/quasar.variables.styl')
},
// Progress Bar Webpack plugin format
// https://github.com/clessg/progress-bar-webpack-plugin#options
progressFormat: ' [:bar] ' + ':percent'.bold + ' (:msg)',
// Default theme to build with ('ios' or 'mat')
defaultTheme: 'mat',
build: {
env: require('./prod.env'),
publicPath: '',
productionSourceMap: (process.env['CSC_SOURCE_MAP'])? true : false,
// Remove unused CSS
// Disable it if it has side-effects for your specific app
purifyCSS: true
},
dev: {
env: require('./dev.env'),
cssSourceMap: true,
// auto open browser or not
openBrowser: true,
publicPath: '/',
port: 8080,
// If for example you are using Quasar Play
// to generate a QR code then on each dev (re)compilation
// you need to avoid clearing out the console, so set this
// to "false", otherwise you can set it to "true" to always
// have only the messages regarding your last (re)compilation.
clearConsoleOnRebuild: false,
// Proxy your API if using any.
// Also see /build/script.dev.js and search for "proxy api requests"
// https://github.com/chimurai/http-proxy-middleware
proxyTable: {}
}
}
/*
* proxyTable example:
*
proxyTable: {
// proxy all requests starting with /api
'/api': {
target: 'https://some.address.com/api',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
*/

@ -0,0 +1,3 @@
module.exports = {
NODE_ENV: '"production"'
}

3
debian/control vendored

@ -4,6 +4,7 @@ Priority: extra
Maintainer: Sipwise Development Team <support@sipwise.com>
Build-Depends:
debhelper (>= 9),
nodejs (>= 4.6.2~),
Standards-Version: 3.9.7
Homepage: https://sipwise.com/
@ -13,4 +14,4 @@ Depends:
${misc:Depends},
${shlibs:Depends},
Description: Customer Self-Care Web UI
New customer Self-Care Web UI as a replacemdent of deprecated ngcp-csc
New customer Self-Care Web UI as a replacement of deprecated ngcp-csc

1
debian/dirs vendored

@ -0,0 +1 @@
usr/share/ngcp-csc

1
debian/install vendored

@ -0,0 +1 @@
csc usr/share/ngcp-csc

8
debian/rules vendored

@ -1,6 +1,12 @@
#!/usr/bin/make -f
# Uncomment this to turn on verbose mode.
# export DH_VERBOSE=1
%:
dh $@
dh "$@"
override_dh_auto_install:
npm install
npm run build
mv dist csc

@ -0,0 +1,64 @@
#!/bin/bash
CSC_SYS_PATH="/usr/share/ngcp-csc/csc"
CSC_TMP_PATH="/usr/share/ngcp-csc/csc.orig"
CSC_PATH="$PWD/dist"
if [ ! -e "$CSC_SYS_PATH" ]; then
mkdir -p "$CSC_SYS_PATH"
fi
CSC_LINK_TARGET=""
if [ -L "$CSC_SYS_PATH" ]; then
CSC_LINK_TARGET=$(readlink "$CSC_SYS_PATH")
fi
ARGS="$*";
case "$1" in
reset)
if [ -L "$CSC_SYS_PATH" ]; then
rm "$CSC_SYS_PATH"
echo "Removed link to development version $CSC_SYS_PATH -> $CSC_LINK_TARGET"
fi
if [ -d "$CSC_TMP_PATH" ]; then
mv "$CSC_TMP_PATH" "$CSC_SYS_PATH"
echo "Restored release version $CSC_TMP_PATH -> $CSC_SYS_PATH"
fi
;;
*)
i=0
while [ "$i" -lt "$#" ]; do
next=$(("$i + 1"))
case "${ARGS[$i]}" in
-p)
if [ "${ARGS[$next]}" != "" ]; then
CSC_PATH="${ARGS[$next]}"
else
echo "Path to development version must not be empty" >&2
exit 1;
fi
;;
-*)
echo "Invalid option ${ARGS[$i]}" >&2
exit 1;
;;
esac
i=$(("$i + 1"))
done
if [ ! -d "$CSC_PATH" ]; then
echo "Path to development version is not a directory '$CSC_PATH'" >&2
exit 1;
fi
if [ ! -L "$CSC_SYS_PATH" -a ! -d "$CSC_TMP_PATH" ]; then
mv "$CSC_SYS_PATH" "$CSC_TMP_PATH"
ln -s -f "$CSC_PATH" "$CSC_SYS_PATH"
fi
echo "Link to development version $CSC_SYS_PATH -> $(readlink ${CSC_SYS_PATH})"
echo "Release temporary moved to $CSC_TMP_PATH"
;;
esac

@ -0,0 +1,56 @@
'use strict';
var webpackCsc = require('./build/webpack.base.conf');
module.exports = function(config) {
config.set({
basePath: '',
files: [
'./t/**/*.js'
],
frameworks: ['mocha'],
plugins : [
'karma-mocha',
'karma-webpack',
'karma-chrome-launcher',
'karma-firefox-launcher',
'karma-junit-reporter',
],
browsers: ['ChromeWebRTC', 'FirefoxWebRTC'],
customLaunchers: {
ChromeWebRTC: {
base: 'Chrome',
flags: [
'--disable-web-security',
'--use-fake-device-for-media-stream',
'--use-fake-ui-for-media-stream',
'--ignore-certificate-errors',
'--no-sandbox',
'--disable-gpu'
]
},
FirefoxWebRTC: {
base: 'Firefox',
prefs: {
'media.navigator.permission.disabled': true,
'media.navigator.streams.fake': true
}
}
},
reporters: [
'progress',
'junit'
],
junitReporter: {
outputDir: './t/'
},
preprocessors: {
'./src/**/*.js': ['webpack'],
'./t/**/*.js': ['webpack']
},
webpack: {
module: webpackCsc.module,
plugins: webpackCsc.plugins
}
});
};

14066
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

@ -0,0 +1,87 @@
{
"name": "ngcp-csc-ui",
"productName": "Customer Self-Care Web UI",
"version": "0.0.6",
"description": "Customer Self-Care Web UI",
"author": "Hans-Peter Herzog <hherzog@sipwise.com>",
"scripts": {
"clean": "node build/script.clean.js",
"dev": "node build/script.dev.js",
"build": "node build/script.build.js mat",
"dev-build": "CSC_SOURCE_MAP=1 CSC_WATCH=1 node build/script.build.js mat",
"lint": "eslint --ext .js,.vue src",
"test": "karma start ./karma.js --single-run"
},
"dependencies": {
"ansi-html": "0.0.7",
"ansi-regex": "^3.0.0",
"babel-runtime": "^6.25.0",
"core-js": "^2.5.1",
"html-entities": "^1.2.1",
"lodash": "^4.17.4",
"quasar-extras": "0.x",
"quasar-framework": "^0.14.4",
"strip-ansi": "^4.0.0",
"vue": "~2.3.4",
"vue-i18n": "^7.3.0",
"vue-resource": "^1.3.4",
"vue-router": "^2.7.0",
"vuex": "^2.4.1",
"vuex-router-sync": "^4.3.2"
},
"devDependencies": {
"autoprefixer": "^6.4.0",
"babel-core": "^6.0.0",
"babel-eslint": "^7.0.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-runtime": "^6.0.0",
"babel-preset-es2015": "^6.0.0",
"babel-preset-stage-2": "^6.0.0",
"chai": "^4.1.2",
"colors": "^1.1.2",
"connect-history-api-fallback": "^1.1.0",
"css-loader": "^0.28.7",
"es6-promise": "^4.1.1",
"eslint": "^4.8.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.9.0",
"eslint-plugin-html": "^3.2.2",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"eventsource-polyfill": "^0.9.6",
"express": "^4.16.1",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^0.11.1",
"friendly-errors-webpack-plugin": "^1.1.3",
"glob": "^7.1.2",
"html-webpack-plugin": "^2.30.1",
"http-proxy-middleware": "^0.17.0",
"json-loader": "^0.5.7",
"karma": "^1.7.1",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.0.1",
"karma-junit-reporter": "^1.2.0",
"karma-mocha": "^1.3.0",
"karma-webpack": "^2.0.4",
"mocha": "^4.0.0",
"opn": "^5.0.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"postcss-loader": "^1.0.0",
"progress-bar-webpack-plugin": "^1.10.0",
"purify-css": "^1.2.6",
"shelljs": "^0.7.0",
"stylus": "^0.54.5",
"stylus-loader": "^3.0.1",
"url-loader": "^0.5.7",
"vue-loader": "^13.0.5",
"vue-style-loader": "^3.0.3",
"vue-template-compiler": "~2.3.4",
"webpack": "^3.6.0",
"webpack-dev-middleware": "^1.12.0",
"webpack-hot-middleware": "^2.19.1",
"webpack-merge": "^4.1.0"
}
}

@ -0,0 +1,21 @@
<template>
<div id="q-app">
<router-view />
</div>
</template>
<script>
import { QTransition } from 'quasar'
export default {
components: {
QTransition
}
}
</script>
<style>
.q-field-icon,
.q-item-icon {
font-size: 24px;
}
</style>

@ -0,0 +1,54 @@
import Vue from 'vue';
export function getGroups(options) {
return new Promise((resolve, reject)=>{
Promise.props({
numbers: Vue.http.get('/api/numbers'),
subscribers: Vue.http.get('/api/subscribers', {
params: {
is_pbx_group: true
}
})
}).then((result)=>{
}).then(()=>{
resolve();
}).catch((err)=>{
reject(err)
});
});
}
export function createGroup(options) {
}
export function saveGroup(options) {
}
export function getSeats() {
}
export function createSeat() {
}
export function saveSeat() {
}
export function getDevices() {
}
export function createDevice() {
}
export function saveDevice() {
}

@ -0,0 +1,80 @@
import _ from 'lodash';
import Vue from 'vue';
export function login(username, password) {
return new Promise((resolve, reject)=>{
var jwt = null;
var subscriberId = null;
Vue.http.post('/login_jwt', {
username: username,
password: password
}).then((result)=>{
jwt = result.body.jwt;
subscriberId = result.body.subscriber_id + "";
resolve({
jwt: jwt,
subscriberId: subscriberId,
});
}).catch((err)=>{
reject(err);
});
});
}
export function getUserData(id) {
return new Promise((resolve, reject)=>{
return Promise.all([
getSubscriberById(id),
getCapabilities(),
getNumbers()
]).then((results)=>{
resolve({
subscriber: results[0],
capabilities: results[1],
numbers: results[2]
});
}).catch((err)=>{
reject(err);
});
});
}
export function getSubscriberById(id) {
return new Promise((resolve, reject)=>{
Vue.http.get('/api/subscribers/' + id).then((result)=>{
var body = JSON.parse(result.body);
resolve(body);
}).catch((err)=>{
reject(err);
});
});
}
export function getCapabilities() {
return new Promise((resolve, reject)=>{
Vue.http.get('/api/capabilities').then((result)=>{
var capabilities = {};
var body = JSON.parse(result.body);
if(_.isArray(body["_embedded"]["ngcp:capabilities"])) {
body['_embedded']['ngcp:capabilities'].forEach((capability)=>{
capabilities[capability.name] = capability.enabled;
});
}
resolve(capabilities);
}).catch((err)=>{
reject(err);
});
});
}
export function getNumbers() {
return new Promise((resolve, reject)=>{
Vue.http.get('/api/numbers').then((result)=>{
// Todo: Check format of numbers
resolve();
}).catch((err)=>{
reject(err);
});
});
}

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="67.407623mm"
height="62.908276mm"
viewBox="0 0 238.84591 222.90334"
id="svg3570"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="quasar-logo-full.svg">
<defs
id="defs3572" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="-39.753589"
inkscape:cy="27.706388"
inkscape:document-units="px"
inkscape:current-layer="g4895-4-4"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1056"
inkscape:window-x="0"
inkscape:window-y="24"
inkscape:window-maximized="1" />
<metadata
id="metadata3575">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-277.71988,-312.33911)">
<g
id="g4895-4-4"
transform="translate(1419.0442,398.9018)">
<g
transform="translate(-29.620665,-4)"
id="g4579-2-20">
<g
id="g4445-2-0"
transform="translate(12.499948,7.809312)">
<g
inkscape:export-ydpi="44.860481"
inkscape:export-xdpi="44.860481"
inkscape:export-filename="/home/emanuele/Desktop/logo1.png"
transform="translate(-712.85583,-503.26814)"
id="g4561-6-7-0">
<g
transform="translate(16.233481,0)"
style="font-style:normal;font-weight:normal;font-size:50.25774765px;line-height:125%;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#263238;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="flowRoot4513-6-6-08">
<path
d="m -402.73125,631.46823 q -0.6125,0.0438 -1.3125,0.0875 -0.65625,0 -1.4,0 l -9.31875,0 q -12.81875,0 -12.81875,-8.44375 l 0,-13.475 q 0,-8.26875 12.6,-8.26875 l 9.75625,0 q 12.6,0 12.6,8.26875 l 0,13.475 q 0,5.03125 -4.4625,7.04375 l 3.10625,2.14375 q 1.35625,0.83125 1.35625,1.70625 0,0.875 -0.7,1.3125 -0.65625,0.48125 -1.88125,0.48125 -0.30625,0 -0.7875,-0.13125 -0.4375,-0.0875 -1.05,-0.48125 l -5.6875,-3.71875 z m 5.38125,-21.74375 q 0,-4.76875 -7.9625,-4.76875 l -9.58125,0 q -7.9625,0 -7.9625,4.76875 l 0,13.3875 q 0,4.94375 8.3125,4.94375 l 8.88125,0 q 8.3125,0 8.3125,-4.94375 l 0,-13.3875 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1"
id="path3428" />
<path
d="m -368.0585,631.64323 q -11.2875,0 -11.2875,-6.9125 l 0,-12.73125 q 0,-1.8375 2.31875,-1.8375 2.31875,0 2.31875,1.8375 l 0,12.775 q 0,3.325 6.475,3.325 l 8.3125,0 q 6.475,0 6.475,-3.325 l 0,-12.775 q 0,-1.8375 2.31875,-1.8375 2.3625,0 2.3625,1.8375 l 0,12.73125 q 0,6.9125 -11.2875,6.9125 l -8.00625,0 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1"
id="path3430" />
<path
d="m -327.2833,631.64323 q -9.3625,0 -9.3625,-5.81875 l 0,-2.49375 q 0,-5.775 9.3625,-5.775 l 18.59375,0 0,-0.65625 q 0,-3.0625 -5.38125,-3.0625 l -6.16875,0 q -2.1875,0 -2.1875,-1.70625 0,-1.75 2.1875,-1.75 l 6.16875,0 q 9.93125,0 9.93125,6.51875 l 0,8.575 q 0,6.16875 -9.5375,6.16875 l -13.60625,0 z m 13.34375,-3.4125 q 5.25,0 5.25,-2.8875 l 0,-4.76875 -18.24375,0 q -5.11875,0 -5.11875,2.66875 l 0,2.275 q 0,2.7125 5.11875,2.7125 l 12.99375,0 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1"
id="path3432" />
<path
d="m -262.77031,626.74323 q 0,4.9 -9.975,4.9 l -17.0625,0 q -2.1875,0 -2.1875,-1.70625 0,-1.70625 2.1875,-1.70625 l 17.0625,0 q 5.38125,0 5.38125,-1.4875 l 0,-2.45 q 0,-1.4875 -5.38125,-1.4875 l -9.0125,0 q -9.975,0 -9.975,-4.76875 l 0,-2.05625 q 0,-5.6 10.28125,-5.6 l 5.99375,0 q 2.23125,0 2.23125,1.75 0,0.875 -0.6125,1.3125 -0.56875,0.39375 -1.61875,0.39375 l -5.99375,0 q -5.73125,0 -5.73125,2.14375 l 0,1.925 q 0,1.79375 5.6875,1.79375 l 9.0125,0 q 9.7125,0 9.7125,4.4625 l 0,2.58125 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1"
id="path3434" />
<path
d="m -241.91709,631.64323 q -9.3625,0 -9.3625,-5.81875 l 0,-2.49375 q 0,-5.775 9.3625,-5.775 l 18.59375,0 0,-0.65625 q 0,-3.0625 -5.38125,-3.0625 l -6.16875,0 q -2.1875,0 -2.1875,-1.70625 0,-1.75 2.1875,-1.75 l 6.16875,0 q 9.93125,0 9.93125,6.51875 l 0,8.575 q 0,6.16875 -9.5375,6.16875 l -13.60625,0 z m 13.34375,-3.4125 q 5.25,0 5.25,-2.8875 l 0,-4.76875 -18.24375,0 q -5.11875,0 -5.11875,2.66875 l 0,2.275 q 0,2.7125 5.11875,2.7125 l 12.99375,0 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1"
id="path3436" />
<path
d="m -205.62285,617.33698 q 0,-6.95625 11.2875,-6.95625 l 3.36875,0 q 2.23125,0 2.23125,1.79375 0,1.79375 -2.23125,1.79375 l -3.54375,0 q -6.475,0 -6.475,3.28125 l 0,12.775 q 0,1.8375 -2.31875,1.8375 -2.31875,0 -2.31875,-1.8375 l 0,-12.6875 z"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:43.75px;font-family:'Neuropol X';-inkscape-font-specification:'Neuropol X';text-align:start;letter-spacing:5px;word-spacing:0px;text-anchor:start;fill:#263238;fill-opacity:1"
id="path3438" />
</g>
</g>
</g>
</g>
<g
id="g5443-0-1-5-1-9"
transform="matrix(0.55595317,0,0,0.55595317,-521.93484,-328.66104)"
inkscape:export-filename="/home/emanuele/Desktop/logo1.png"
inkscape:export-xdpi="44.860481"
inkscape:export-ydpi="44.860481">
<g
inkscape:export-ydpi="3.4165223"
inkscape:export-xdpi="3.4165223"
transform="matrix(0.09527033,0,0,0.09527033,-1695.2716,706.62921)"
id="g8856-6-1-1-9-0-1-9">
<circle
r="1485"
cy="-1361.2571"
cx="8317.3574"
id="circle8858-1-3-7-6-5-3-0"
style="opacity:1;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:50;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-xdpi="10.031387"
inkscape:export-ydpi="10.031387" />
<path
inkscape:export-ydpi="10.031387"
inkscape:export-xdpi="10.031387"
style="opacity:1;fill:#263238;fill-opacity:1;stroke:none;stroke-width:10;stroke-linecap:square;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 8560.3823,-1361.3029 a 242.947,242.947 0 0 1 -242.947,242.948 242.947,242.947 0 0 1 -242.947,-242.948 242.947,242.947 0 0 1 242.947,-242.946 242.947,242.947 0 0 1 242.947,242.946 z"
id="path8860-5-4-8-2-9-0-9"
inkscape:connector-curvature="0" />
<path
id="path8862-5-5-9-1-3-6-3"
d="m 9395.8755,-1984.028 a 1245.372,1245.372 0 0 0 -190.8415,-249.4971 l -280.8618,162.1556 c -87.542,-74.7796 -187.0349,-132.0588 -293.2407,-169.9527 -95.8868,97.1766 -172.0602,205.7604 -226.9672,323.8487 312.6411,-21.2772 635.5313,91.8725 935.2898,326.0721 l 176.7612,-102.0532 a 1245.372,1245.372 0 0 0 -120.1398,-290.5734 z"
clip-path="none"
mask="none"
style="fill:#1976d2;fill-opacity:1"
inkscape:connector-curvature="0"
inkscape:transform-center-x="-514.04855"
inkscape:transform-center-y="-444.04649" />
<path
inkscape:transform-center-y="265.80217"
inkscape:transform-center-x="-689.63727"
inkscape:connector-curvature="0"
style="fill:#42a5f5;fill-opacity:1"
mask="none"
clip-path="none"
d="m 9395.9474,-738.70387 a 1245.372,1245.372 0 0 0 120.6501,-290.02213 l -280.8618,-162.1557 c 20.99,-113.2034 20.8488,-228.0063 0.563,-338.9302 -132.1008,-34.4521 -264.2238,-46.1283 -393.9448,-34.635 174.7471,260.1165 238.2017,596.32248 185.2582,973.02076 l 176.7612,102.05309 a 1245.372,1245.372 0 0 0 191.5741,-249.33082 z"
id="path8864-4-8-1-2-4-4-4" />
<path
id="path8866-7-5-5-0-6-4-7"
d="m 8317.501,-115.97954 a 1245.372,1245.372 0 0 0 311.4916,-40.52501 l 0,-324.31131 c 108.5321,-38.42382 207.8837,-95.94755 293.8037,-168.97752 -36.214,-131.6287 -92.1636,-251.88868 -166.9776,-358.48372 -137.894,281.39369 -397.3296,504.44998 -750.0316,646.9487 l 0,204.10623 a 1245.372,1245.372 0 0 0 311.7139,41.24263 z"
clip-path="none"
mask="none"
style="fill:#1976d2;fill-opacity:1"
inkscape:connector-curvature="0"
inkscape:transform-center-x="-117.49007"
inkscape:transform-center-y="639.34029" />
<path
inkscape:transform-center-y="444.04652"
inkscape:transform-center-x="514.04857"
inkscape:connector-curvature="0"
style="fill:#42a5f5;fill-opacity:1"
mask="none"
clip-path="none"
d="m 7238.9827,-738.57936 a 1245.372,1245.372 0 0 0 190.8415,249.49714 l 280.8618,-162.15566 c 87.5421,74.77965 187.0349,132.05879 293.2407,169.95271 95.8868,-97.17659 172.0602,-205.76036 226.9672,-323.8487 -312.6411,21.27714 -635.5313,-91.87254 -935.2898,-326.07203 l -176.7612,102.0531 a 1245.372,1245.372 0 0 0 120.1398,290.57344 z"
id="path8868-6-7-4-7-2-7-3" />
<path
id="path8870-5-3-9-3-5-5-1"
d="m 7238.9108,-1983.9035 a 1245.372,1245.372 0 0 0 -120.6501,290.0221 l 280.8618,162.1557 c -20.99,113.2035 -20.8488,228.0063 -0.563,338.9302 132.1008,34.4521 264.2238,46.1283 393.9448,34.635 -174.7471,-260.1165 -238.2017,-596.3225 -185.2582,-973.0207 l -176.7612,-102.0532 a 1245.372,1245.372 0 0 0 -191.5741,249.3309 z"
clip-path="none"
mask="none"
style="fill:#1976d2;fill-opacity:1"
inkscape:connector-curvature="0"
inkscape:transform-center-x="689.63729"
inkscape:transform-center-y="-265.80221" />
<path
inkscape:transform-center-y="-639.34032"
inkscape:transform-center-x="117.49005"
inkscape:connector-curvature="0"
style="fill:#42a5f5;fill-opacity:1"
mask="none"
clip-path="none"
d="m 8317.3572,-2606.6279 a 1245.372,1245.372 0 0 0 -311.4915,40.525 l -1e-4,324.3113 c -108.5321,38.4239 -207.8837,95.9476 -293.8037,168.9776 36.214,131.6287 92.1637,251.8886 166.9776,358.4837 137.894,-281.3937 397.3296,-504.45 750.0316,-646.9487 l 1e-4,-204.1063 a 1245.372,1245.372 0 0 0 -311.714,-41.2426 z"
id="path8872-6-3-2-1-3-3-7" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

@ -0,0 +1,10 @@
<template>
<div>404 NotFound</div>
</template>
<script>
export default {}
</script>
<style>
</style>

@ -0,0 +1,105 @@
<template>
<q-layout>
<div id="csc-login" class="row">
<div class="column col-lg-4 col-xl-4 col-md-3 gt-sm"></div>
<div class="column col-12 col-md-6 col-lg-4 col-xl-4">
<q-card>
<q-card-title>
<q-icon name="" />{{ $t('login_title') }}
<span slot="subtitle"></span>
</q-card-title>
<q-card-main>
<q-field icon="fa-user-circle" :helper="$t('username_helper')" :count="128">
<q-input type="text" max-length="128" :float-label="$t('username')" clearable v-model="username"/>
</q-field>
<q-field icon="fa-lock" :helper="$t('password_helper')" :count="32">
<q-input type="password" max-length="32" :float-label="$t('password')" clearable v-model="password"/>
</q-field>
</q-card-main>
<q-card-actions class="pull-right">
<q-btn flat icon-right="fa-arrow-right" color="primary" @click="login()">{{ $t('login_button') }}</q-btn>
</q-card-actions>
</q-card>
</div>
<div class="column col-lg-4 col-xl-4 col-md-3 gt-sm"></div>
</div>
</q-layout>
</template>
<script>
import { startLoading, stopLoading, showGlobalError } from '../helpers/ui'
import {
QLayout,
QCard,
QCardTitle,
QCardSeparator,
QCardMain,
QField,
QInput,
QCardActions,
QBtn,
QIcon,
Loading,
Alert } from 'quasar'
export default {
name: 'login',
components: {
QLayout,
QCard,
QCardTitle,
QCardSeparator,
QCardMain,
QField,
QInput,
QCardActions,
QBtn,
QIcon
},
data () {
return {
user: '',
pass: ''
}
},
methods: {
login() {
startLoading();
this.$store.dispatch('user/login').then(()=>{
stopLoading();
this.$router.push({path : '/'});
}).catch((err)=>{
stopLoading();
showGlobalError(this.$i18n.t('login_error'));
});
}
},
computed: {
username: {
get () {
return this.$store.state.user.username;
},
set (value) {
this.$store.commit('user/updateUsername', value)
}
},
password: {
get () {
return this.$store.state.user.password;
},
set (value) {
this.$store.commit('user/updatePassword', value)
}
}
}
}
</script>
<style>
#csc-login {
padding-top: 15%;
}
#csc-login .q-card-container {
}
</style>

@ -0,0 +1,230 @@
<template>
<q-layout ref="layout" view="lHr LpR lFr" :right-breakpoint="1100">
<q-toolbar slot="header">
<q-btn flat @click="$refs.layout.toggleLeft()">
<q-icon name="menu"/>
</q-btn>
<q-toolbar-title>
{{ $t('title') }}
<span slot="subtitle"></span>
</q-toolbar-title>
<q-btn flat @click="" icon-right="fa-user-circle">
<span id="user-login-as">{{ $t('loggedInAs') }}</span><span id="user-name">{{ getUsername }}</span>
<q-popover ref="popover">
<q-list item-separator link>
<q-item @click="logout()">
<q-item-main label="Logout" />
<q-item-side icon="exit to app"/>
</q-item>
</q-list>
</q-popover>
</q-btn>
</q-toolbar>
<q-list id="main-menu" slot="left" no-border link inset-delimiter>
<q-side-link item to="/user/conversations">
<q-item-side icon="question answer"></q-item-side>
<q-item-main :label="$t('mainNavigation.conversations.title')"
:sublabel="$t('mainNavigation.conversations.subTitle')"/>
</q-side-link>
<q-collapsible :opened="isCallForward" intend icon="fa-angle-double-right"
:label="$t('mainNavigation.callForward.title')"
:sublabel="$t('mainNavigation.callForward.subTitle')">
<q-side-link item to="/user/call-forward/always">
<q-item-side icon="check circle"/>
<q-item-main :label="$t('mainNavigation.callForward.always')"/>
</q-side-link>
<q-side-link item to="/user/call-forward/company-hours">
<q-item-side icon="schedule"/>
<q-item-main :label="$t('mainNavigation.callForward.companyHours')"/>
</q-side-link>
<q-side-link item to="/user/call-forward/after-hours">
<q-item-side icon="watch later"/>
<q-item-main :label="$t('mainNavigation.callForward.afterHours')"/>
</q-side-link>
</q-collapsible>
<q-collapsible :opened="isCallBlocking" intend icon="fa-ban"
:label="$t('mainNavigation.callBlocking.title')"
:sublabel="$t('mainNavigation.callBlocking.subTitle')">
<q-side-link item to="/user/call-blocking/incoming">
<q-item-side icon="fa-arrow-circle-o-left"/>
<q-item-main :label="$t('mainNavigation.callBlocking.incoming')"/>
</q-side-link>
<q-side-link item to="/user/call-blocking/outgoing">
<q-item-side icon="fa-arrow-circle-o-right"/>
<q-item-main :label="$t('mainNavigation.callBlocking.outgoing')"/>
</q-side-link>
<q-side-link item to="/user/call-blocking/privacy">
<q-item-side icon="fa-user-secret"/>
<q-item-main :label="$t('mainNavigation.callBlocking.privacy')"/>
</q-side-link>
</q-collapsible>
<q-side-link item to="/user/reminder">
<q-item-side icon="fa-bell"/>
<q-item-main
label="Reminder"
sublabel="Set your personal alarm"/>
</q-side-link>
<q-collapsible v-if="isPbxAdmin" :opened="isPbxConfiguration" intend icon="fa-gear"
:label="$t('mainNavigation.pbxConfiguration.title')"
:sublabel="$t('mainNavigation.pbxConfiguration.subTitle')">
<q-side-link item to="/user/pbx-configuration/groups">
<q-item-side icon="fa-group"/>
<q-item-main :label="$t('mainNavigation.pbxConfiguration.groups')"/>
</q-side-link>
<q-side-link item to="/user/pbx-configuration/seats">
<q-item-side icon="fa-home"/>
<q-item-main :label="$t('mainNavigation.pbxConfiguration.seats')"/>
</q-side-link>
<q-side-link item to="/user/pbx-configuration/devices">
<q-item-side icon="fa-fax"/>
<q-item-main :label="$t('mainNavigation.pbxConfiguration.devices')"/>
</q-side-link>
</q-collapsible>
</q-list>
<q-fixed-position corner="top-right" :offset="[20, 20]">
<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>
</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>
<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>
</q-fab>
</q-fixed-position>
<router-view />
</q-layout>
</template>
<script>
import _ from 'lodash';
import { startLoading, stopLoading, showGlobalError } from '../../helpers/ui'
import { mapState, mapGetters } from 'vuex'
import {
QLayout,
QToolbar,
QToolbarTitle,
QBtn,
QIcon,
QList,
QListHeader,
QItem,
QItemSide,
QItemMain,
QPopover,
QFab,
QFabAction,
QFixedPosition,
QTooltip,
QSideLink,
QTransition,
QCollapsible
} from 'quasar'
export default {
name: 'default',
mounted: function() {
this.$refs.layout.showLeft();
// this.$store.dispatch('connectRtcEngine');
},
created: function() {
this.$store.dispatch('user/initUser');
},
components: {
QLayout,
QToolbar,
QToolbarTitle,
QBtn,
QIcon,
QList,
QListHeader,
QItem,
QItemSide,
QItemMain,
QPopover,
QFab,
QFabAction,
QFixedPosition,
QTooltip,
QSideLink,
QTransition,
QCollapsible
},
computed: {
...mapGetters('user', ['getUsername', 'isPbxAdmin']),
...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')
})
},
methods: {
logout() {
startLoading();
this.$store.dispatch('user/logout').then(()=>{
stopLoading();
this.$router.push({path: '/login'});
})
},
startCall() {
if(!this.$store.state.rtcEngineConnected) {
showGlobalError(this.$t('rtcEngineDisconnected'));
}
},
navigate(path) {
this.$router.push({path: path});
}
}
}
</script>
<style>
#main-menu {
padding-top:60px;
}
#main-menu .q-item-side {
min-width: 30px;
}
#main-menu .q-item {
padding: 12px 24px;
}
#main-menu .router-link-active,
#main-menu .q-item:hover {
background-color: #475360;
}
#main-menu .q-item .q-item-sublabel {
color: #5b7086;
}
#main-menu .q-item .q-item-main,
#main-menu .q-item .q-item-side {
color: #ADB3B8;
}
#main-menu .q-collapsible-sub-item {
padding: 0;
}
#main-menu .q-collapsible-sub-item .q-item {
padding-left: 60px;
}
#user-login-as {
display: inline-block;
text-transform: none;
color: #c5eab4;
}
#user-login-as:after {
content: " ";
white-space: pre;
}
#user-name {
font-weight: bold;
}
</style>

@ -0,0 +1,14 @@
<template>
<div>CallBlocking Incoming</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>CallBlocking Outgoing</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>CallBlocking Privacy</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>CallForward AfterHours</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>CallForward Always</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>CallForward CompanyHours</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>Conversations</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>PBX Devices</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>PBX Groups</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>PBX Seats</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,14 @@
<template>
<div>Reminder</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,21 @@
import { Loading, Alert } from 'quasar'
export function startLoading() {
Loading.show({ delay: 0 });
}
export function stopLoading() {
Loading.hide();
}
export function showGlobalError(message) {
const alert = Alert.create({
html: message,
position: 'top-center',
enter: 'bounceIn',
leave: 'fadeOut'
});
setTimeout(()=>{ alert.dismiss(); }, 2000);
}

@ -0,0 +1,17 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n);
export const locales = {
en: require('./locales/en')
};
export const i18n = new VueI18n({
locale: localStorage.getItem('lang') || navigator.language || navigator.userLanguage,
fallbackLocale: 'en',
messages: locales
});

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<title>Quasar App</title>
<link rel="icon" href="statics/quasar-logo.png" type="image/x-icon">
</head>
<body>
<div id="q-app"></div>
<!-- built files will be auto injected -->
</body>
</html>

@ -0,0 +1,45 @@
{
"title": "Sipwise Customer Portal",
"login_title": "Customer Self Care Portal",
"login_button": "Sign In",
"login_error": "Wrong username or password",
"username": "Username",
"username_helper": "Web-Username, SIP-URI",
"password": "Password",
"password_helper": "Web-Password",
"rtcEngineDisconnected": "You can not start a call. Service ist currently unavailable.",
"startCall": "Start Call",
"sendSms": "Send SMS",
"sendFax": "Send Fax",
"mainNavigation": {
"conversations":{
"title": "Conversations",
"subTitle": "Calls, SMS, VoiceMails"
},
"callForward":{
"title": "Call Forward",
"subTitle": "Control your calls",
"always": "Always",
"companyHours": "Company Hours",
"afterHours": "After Hours"
},
"callBlocking": {
"title": "Call Blocking",
"subTitle": "Block numbers",
"incoming": "Incoming",
"outgoing": "Outgoing",
"privacy": "Privacy"
},
"reminder": {
"title": "Reminder"
},
"pbxConfiguration":{
"title": "PBX Configuration",
"subTitle": "Groups, Devices",
"groups": "Groups",
"seats": "Seats",
"devices": "Devices"
}
},
"loggedInAs": "Logged in as"
}

@ -0,0 +1,57 @@
// === DEFAULT / CUSTOM STYLE ===
// WARNING! always comment out ONE of the two require() calls below.
// 1. use next line to activate CUSTOM STYLE (./src/themes)
require(`./themes/app.${__THEME}.styl`)
// 2. or, use next line to activate DEFAULT QUASAR STYLE
// require(`quasar/dist/quasar.${__THEME}.css`)
// ==============================
// Uncomment the following lines if you need IE11/Edge support
// require(`quasar/dist/quasar.ie`)
// require(`quasar/dist/quasar.ie.${__THEME}.css`)
import _ from 'lodash'
import Vue from 'vue'
import VueResource from 'vue-resource'
import Quasar from 'quasar'
import { store } from './store'
import { i18n, locales } from './i18n'
import router from './router'
import { sync } from 'vuex-router-sync'
Vue.use(VueResource);
Vue.config.productionTip = false;
Vue.use(Quasar); // Install Quasar Framework
if (__THEME === 'mat') {
require('quasar-extras/roboto-font')
}
import 'quasar-extras/material-icons'
// import 'quasar-extras/ionicons'
import 'quasar-extras/fontawesome'
import 'quasar-extras/animate'
sync(store, router);
Vue.http.interceptors.push(function(request, next) {
var jwt = localStorage.getItem('jwt');
if(!_.isEmpty(jwt)) {
request.headers.set('Authorization', 'Bearer ' + jwt);
}
if(request.method === 'POST' && _.isEmpty(request.body)) {
request.body = {};
}
next();
});
Quasar.start(() => {
new Vue({
el: '#q-app',
i18n,
store,
router,
render: h => h(require('./App.vue').default)
})
});

@ -0,0 +1,38 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import { store } from './store'
import routes from './routes'
Vue.use(VueRouter);
var router = new VueRouter({
/*
* NOTE! VueRouter "history" mode DOESN'T works for Cordova builds,
* it is only to be used only for websites.
*
* If you decide to go with "history" mode, please also open /config/index.js
* and set "build.publicPath" to something other than an empty string.
* Example: '/' instead of current ''
*
* If switching back to default "hash" mode, don't forget to set the
* build publicPath back to '' so Cordova builds work again.
*/
routes: routes
});
router.beforeEach((to, from, next) => {
if (!store.getters['user/isLogged'] && to.path !== '/login') {
next({
path: '/login'
});
} else if (store.getters['user/isLogged'] && to.path === '/login') {
next({
path: '/'
});
} else {
next();
}
});
export default router;

@ -0,0 +1,64 @@
export default [
{
path: '/user',
component: require('./components/layouts/Default').default,
children: [
{
path: 'conversations',
component: require('./components/pages/Conversations').default,
},
{
path: 'call-forward/always',
component: require('./components/pages/CallForward/Always').default,
},
{
path: 'call-forward/company-hours',
component: require('./components/pages/CallForward/CompanyHours').default
},
{
path: 'call-forward/after-hours',
component: require('./components/pages/CallForward/AfterHours').default
},
{
path: 'call-blocking/incoming',
component: require('./components/pages/CallBlocking/Incoming').default
},
{
path: 'call-blocking/outgoing',
component: require('./components/pages/CallBlocking/Outgoing').default
},
{
path: 'call-blocking/privacy',
component: require('./components/pages/CallBlocking/Privacy').default
},
{
path: 'reminder',
component: require('./components/pages/Reminder').default},
{
path: 'pbx-configuration/groups',
component: require('./components/pages/PbxConfiguration/Groups').default
},
{
path: 'pbx-configuration/seats',
component: require('./components/pages/PbxConfiguration/Seats').default
},
{
path: 'pbx-configuration/devices',
component: require('./components/pages/PbxConfiguration/Devices').default
}
]
},
{
path: '/login',
component: require('./components/Login').default
},
{
path: '/',
redirect: {path:'/user/conversations'}
},
{
path: '*',
component: require('./components/Error404').default
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

@ -0,0 +1,105 @@
'use strict';
import _ from 'lodash'
import Vue from 'vue'
import Vuex from 'vuex'
// import cdk from 'cdk';
var rtcEngineClient = null;
var rtcEngineNetwork = null;
Vue.use(Vuex);
export const store = new Vuex.Store({
modules: {
user: require('./user').UserModule
},
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();
}
});
}
}
});

@ -0,0 +1,30 @@
import { getGroups } from '../../api/pbx-config'
export const PbxGroups = {
state: {
groups: [],
page: 1
},
getters: {
},
mutations: {
showGroups: function(state, options) {
state.groups = options.groups;
}
},
actions: {
loadGroups: function(context, options) {
return new Promise((resolve, reject)=>{
getGroups().then((groups)=>{
context.commit('showGroups', {
groups: groups
});
}).catch((err)=>{
reject(err);
});
});
}
}
};

@ -0,0 +1,106 @@
'use strict';
import _ from 'lodash';
import { login, getCapabilities, getUserData} from '../api/user';
import Vue from 'vue';
// import cdk from 'cdk';
var rtcEngineClient = '';
export const UserModule = {
namespaced: true,
state: {
username: '',
password: '',
jwt: localStorage.getItem('jwt') || null,
subscriberId: localStorage.getItem('subscriberId') || null,
loggedUsername: '',
subscriber: null,
capabilities: null,
numbers: null
},
getters: {
isLogged(state, getters) {
return !_.isEmpty(state.jwt) && !_.isEmpty(state.subscriberId);
},
getUsername(state, getters) {
if(state.subscriber !== null && !_.isEmpty(state.subscriber.display_name)) {
return state.subscriber.display_name;
} else if (state.subscriber !== null) {
return state.subscriber.username + "@" + state.subscriber.domain;
} else {
return "";
}
},
isAdmin(state, getters) {
return state.subscriber !== null && state.subscriber.administrative;
},
isPbxAdmin(state, getters) {
return getters.isAdmin && state.capabilities !== null && state.capabilities.cloudpbx;
}
},
mutations: {
login(state, options) {
state.jwt = options.jwt;
state.subscriberId = options.subscriberId;
},
setUserData(state, options) {
state.subscriber = options.subscriber;
state.capabilities = options.capabilities;
state.numbers = options.numbers;
},
logout(state) {
state.jwt = null;
state.subscriberId = null;
},
updatePassword (state, password) {
state.password = password;
},
updateUsername (state, username) {
state.username = username;
}
},
actions: {
login(context) {
return new Promise((resolve, reject)=>{
login(context.state.username, context.state.password).then((result)=>{
localStorage.setItem('jwt', result.jwt);
localStorage.setItem('subscriberId', result.subscriberId);
context.commit('login', {
jwt: localStorage.getItem('jwt'),
subscriberId: localStorage.getItem('subscriberId')
});
resolve();
}).catch((err)=>{
reject(err);
});
});
},
logout(context) {
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();
});
},
initUser(context) {
return new Promise((resolve, reject)=>{
getUserData(localStorage.getItem('subscriberId')).then((result)=>{
context.commit('setUserData', {
subscriber: result.subscriber,
capabilities: result.capabilities,
numbers: result.numbers
});
resolve();
}).catch((err)=>{
reject(err);
});
});
}
}
};

@ -0,0 +1,14 @@
// This file is included in the build if src/main.js imports it.
// Otherwise the default iOS CSS file is bundled.
// Check "DEFAULT / CUSTOM STYLE" in src/main.js
// App Shared Variables
// --------------------------------------------------
// Shared Stylus variables go in the app.variables.styl file
@import 'app.variables'
// Quasar iOS Design Stylus
// --------------------------------------------------
// Custom App variables must be declared before importing Quasar.
// Quasar will use its default values when a custom variable isn't provided.
@import '~quasar-framework/dist/quasar.ios.styl'

@ -0,0 +1,21 @@
// This file is included in the build if src/main.js imports it.
// Otherwise the default Material CSS file is bundled.
// Check "DEFAULT / CUSTOM STYLE" in src/main.js
// App Shared Variables
// --------------------------------------------------
// Shared Stylus variables go in the app.variables.styl file
@import 'app.variables'
// Quasar Material Design Stylus
// --------------------------------------------------
// Custom App variables must be declared before importing Quasar.
// Quasar will use its default values when a custom variable isn't provided.
@import '~quasar-framework/dist/quasar.mat.styl'
.q-fab-active-icon.material-icons,
.q-fab-icon.material-icons {
display: inherit;
}

@ -0,0 +1,38 @@
// This file is included in the build if src/main.js imports
// either app.mat.styl or app.ios.styl.
// Check "DEFAULT / CUSTOM STYLE" in src/main.js
// App Shared Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Stylus variables found in Quasar's source Stylus files. Setting
// variables before Quasar's Stylus will use these variables rather than
// Quasar's default Stylus variable values. Stylus variables specific
// to the themes belong in either the app.ios.styl or app.mat.styl files.
// App Shared Color Variables
// --------------------------------------------------
// It's highly recommended to change the default colors
// to match your app's branding.
$primary = #66A648
$secondary = #26A69A
$tertiary = #555
$neutral = #E0E1E2
$positive = #21BA45
$negative = #DB2828
$info = #31CCEC
$warning = #F2C037
$toolbar-background = #66A648
$toolbar-min-height = 60px
$layout-header-shadow = $no-shadow
$layout-aside-shadow = $no-shadow
$layout-aside-left-width = 260px
$layout-aside-background = #32404E
$layout-footer-shadow = $no-shadow

@ -0,0 +1,24 @@
//
// Webpack alias "variables" points to this file.
// So you can import it in your app's *.vue files
// inside the <style> tag like below.
//
// NOTICE that you need lang="styl"
//
// <style lang="styl">
// @import '~variables'
// ........
// </style>
// First we load app's Stylus variables
@import 'app.variables'
// Then we load Quasar Stylus variables.
// Any variables defined in "app.variables.styl"
// will override Quasar's ones.
//
// NOTICE that we only import Core Quasar Variables
// like colors, media breakpoints, and so.
// No component variable will be included.
@import '~quasar/dist/core.variables'

@ -0,0 +1,63 @@
# DOCKER_NAME=ngcp-csc-ui-stretch
FROM docker.mgm.sipwise.com/sipwise-stretch:latest
# Important! Update this no-op ENV variable when this Dockerfile
# is updated with the current date. It will force refresh of all
# of the base images and things like `apt-get update` won't be using
# old cached versions when the Dockerfile is built.
ENV REFRESHED_AT 2017-10-06
ENV DEBIAN_FRONTEND noninteractive
ENV DISPLAY=:0
# files that get-code generates
COPY t/sources.list.d/builddeps.list /etc/apt/sources.list.d/
COPY t/sources.list.d/preferences /etc/apt/preferences.d/
RUN apt-get update && apt-get install --assume-yes \
devscripts \
firefox-esr \
gtk2-engines-pixbuf \
libgconf-2-4 \
net-tools \
nodejs \
wget \
xterm \
xvfb
RUN wget -q https://deb.sipwise.com/files/google-chrome-stable_59.0.3071.115-1_amd64.deb
RUN dpkg --force-depends -i google-chrome-stable_59.0.3071.115-1_amd64.deb || true
RUN apt-get --assume-yes -f install
RUN echo "cd /code && ./t/testrunner" >/root/.bash_history
# we cannot use /code/ here otherwise it will be 'mounted over' with following 'docker run'
ADD package.json /tmp/
ADD npm-shrinkwrap.json /tmp/
ADD README.md /tmp/
WORKDIR /tmp
RUN npm install /tmp
WORKDIR /code
################################################################################
# Instructions for usage
# ----------------------
# When you want to build the base image from scratch
# (jump to the next section if you don't want to build yourself!):
#
# you need to put the proper NGCP sources at t/sources.list.d/builddeps.list
# for instance, trunk:
# echo "deb https://deb.sipwise.com/autobuild/ release-trunk-stretch main" > t/sources.list.d/builddeps.list
#
# NOTE: run the following command from root folder of git repository:
# % docker build --tag="ngcp-csc-ui-stretch" -f ./t/Dockerfile .
# % docker run --rm -i -t -v $(pwd):/code:ro ngcp-csc-ui-stretch:latest bash
#
# Use the existing docker image:
# % docker pull docker.mgm.sipwise.com/ngcp-csc-ui-stretch
# NOTE: run the following command from root folder of git repository:
# % docker run --rm -i -t -v $(pwd):/code:ro -v /results docker.mgm.sipwise.com/ngcp-csc-ui-stretch:latest bash
#
# Inside docker:
# cd /code && ./t/testrunner
################################################################################

@ -0,0 +1,17 @@
'use strict';
import { UserModule } from '../../src/store/user';
import { assert } from 'chai';
describe('UserModule', ()=>{
it('should login', ()=>{
var state = {};
UserModule.mutations.login(state, {
jwt: 'abc123',
subscriberId: 123
});
assert.equal(state.jwt, 'abc123');
assert.equal(state.subscriberId, '123');
});
});

@ -0,0 +1,29 @@
#!/bin/bash
if ! [ -f /.dockerenv ] && ! grep -q 'devices:/docker' /proc/1/cgroup ; then
echo "Not running inside docker, exiting to avoid data damage." >&2
exit 1
fi
set -e
set -u
echo "### Starting display server"
Xvfb -ac :0 -screen 0 1280x1024x16 &
trap 'killall -9 Xvfb' EXIT
echo "### Copying and moving files"
cp -Rf . /tmp/code
ln -s /tmp/node_modules /tmp/code/node_modules
cd /tmp/code/
echo "################################################################################"
echo "Finished main setup, now running tests ..."
npm test
echo "### Moving JUnit XML files to /results"
mv /tmp/code/t/*.xml /results
echo "Finished test execution, test execution returned with exit code."
echo "################################################################################"

@ -0,0 +1,14 @@
<template>
<div></div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,73 @@
<template>
<!-- Configure "view" prop for QLayout -->
<q-layout>
<q-toolbar slot="header">
<!-- opens drawer below
<button class="hide-on-drawer-visible" @click="$refs.drawer.open()">
<i>menu</i>
</button>
-->
<q-toolbar-title>
Title
</q-toolbar-title>
</q-toolbar>
<!-- Navigation Tabs
<q-tabs slot="navigation">
<q-route-tab slot="title" icon="view_quilt" to="/layout/about" replace hide="icon" label="About" />
<q-route-tab slot="title" icon="view_day" to="/layout/toolbar" replace hide="icon" label="Toolbar" />
<q-route-tab slot="title" icon="view_day" to="/layout/tabs" replace label="Tabs" />
<q-route-tab slot="title" icon="input" to="/layout/drawer" replace label="Drawer" />
</q-tabs>
-->
<!-- Left Side Panel
<div slot="left">
<q-list no-border link inset-delimiter>
<q-list-header>Essential Links</q-list-header>
<q-item>
<q-item-side icon="school" />
<q-item-main label="Docs" sublabel="quasar-framework.org" />
</q-item>
<q-item>
<q-item-side icon="record_voice_over" />
<q-item-main label="Forum" sublabel="forum.quasar-framework.org" />
</q-item>
<q-item>
<q-item-side icon="chat" />
<q-item-main label="Gitter Channel" sublabel="Quasar Lobby" />
</q-item>
<q-item>
<q-item-side icon="rss feed" />
<q-item-main label="Twitter" sublabel="@quasarframework" />
</q-item>
</q-list>
</div>
-->
<!-- Right Side Panel
<div slot="right">
...
</div>
-->
<router-view />
<!-- Footer
<q-toolbar slot="footer">
...
</q-toolbar>
-->
</q-layout>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>

@ -0,0 +1,17 @@
<template>
<!-- if you want automatic padding use "layout-padding" class -->
<div class="layout-padding">
<!-- your content -->
</div>
</template>
<script>
export default {
data () {
return {}
}
}
</script>
<style>
</style>
Loading…
Cancel
Save