TT#95604 Update developer guide

Change-Id: I7b030fb6951d9c1503d535dacae8193a24d4948c
mr9.1.1
Hans-Peter Herzog 5 years ago
parent e24a029a7e
commit 17594233fa

@ -1,214 +1,186 @@
# Customer Self-Care Web UI
## Development
Please follow these steps to set up a development environment for ngcp-csc-ui.
## Development Quick Start for internal developers
### Node and npm
* Check if [internal development server](https://dev-web-trunk.mgm.sipwise.com/) is running
* [Install yarn](https://yarnpkg.com/getting-started/install) on your system
* Run development environment
First make sure you have Node.js and npm installed, and then install and build the app.
`yarn run dev:docker dev-web-trunk.mgm.sipwise.com`
1. Install latest Node.js v6 and npm v3. It's recommended to [install and use nvm](https://github.com/creationix/nvm) to make sure you're using the right versions
1. Clone the ngcp-csc-ui repo locally
1. Run npm install
## Technology
`npm install`
### Vue.js and Quasar
1. Build the app for dev
After a test phase with Sencha Ext JS we decided to use
[Vue.js](https://vuejs.org) in combination with the [Quasar Framework](https://quasar.dev).
`npm run dev-build`
### Developers basics
### Vagrant
We highly recommend the following courses to understand the
principles and ultimately the code:
To run this app you also need to have a [vagrant-ngcp](https://www.sipwise.org/doc/mr4.1.1/spce/ar01s04.html#_vagrant_box_for_virtualbox) environment up and running.
* [Intro to Vue 2](https://www.vuemastery.com/courses/intro-to-vue-js/vue-instance)
* [Real World Vue 2](https://www.vuemastery.com/courses/real-world-vue-js/real-world-intro)
* [Mastering Vuex](https://www.vuemastery.com/courses/mastering-vuex/success-error-notifications)
1. Go to your local vagrant-ngcp folder, for example
In addition, we also recommend the following Quasar Framework tutorials:
`cd ~/Sipwise/mr/vagrant-ngcp`
* [Quasar Video Tutorials](https://quasar.dev/video-tutorials)
1. Spin up a new vagrant box
## Project Guide
`./v n pro1`
### Add a new page
1. SSH into vagrant box and become root
In order to add a new page you need to go along the following steps:
`
./v s pro1
sudo -s
`
* Create a new page component using the following naming pattern
1. Edit config.yml
`src/pages/CscPageNewFeature.vue`
`vim /etc/ngcp-config/config.yml`
* Create a route and add it to the route file
1. Enable csc by finding the the "http_csc:" section of the file, and setting "csc_js_enable:" to yes
`src/router/routes.js`
`csc_js_enable: yes`
```javascript
{
path: '/user/new-feature',
component: CscPageNewFeature,
meta: {
title: i18n.t('pages.newFeature.title'),
subtitle: i18n.t('navigation.newFeature.subTitle')
}
}
```
1. Run ngcpcfg apply
* Add new feature to the main menu
`ngcpcfg apply 'Enable http_csc'`
1. Navigate into the vagrant shared local folder (configured via custom_config file in local vagrant-ngcp/users.d/ folder) and execute dev.sh script to set up symlink between local built files and vagrant files served via nginx
`
cd /usr/local/devel/ngcp-csc-ui/
./dev.sh
`
You can now access ngcp-csc-ui in browser by using the url provided by the vagrant script.
### Enable Call Feature
To enable use of the call feature, you must first both enable it in ngcp-config and change some options in ngcp-panel.
1. Edit config.yml
`vim /etc/ngcp-config/config.yml`
1. Enable rtcengine by finding the "rtcengine:" section of the file, and setting "enable:" to yes
`
rtcengine:
enable: yes
`
1. Run ngcpcfg apply
`ngcpcfg apply 'Enable rtcengine'`
1. Log in to ngcp-panel with administrator credentials
1. Go to "Settings > Resellers""
1. Look for reseller row with "default" written in "Name" column, and click "Edit" for that row
1. Check "Enable rtc" checkbox and save
1. Go to "Settings > Domains"
1. Look domain row with domain linked to bound to kamailio in the "Domain" column (usually the bottom-most one for vagrant-ngcp environments), and click "Preferences" for that row
1. Go to "NAT and Media Flow Control" section
1. Set "use_rtpproxy" to "Always with rtpproxy as additional ICE candidate" and "transport_protocol" to "UDP/TLS/RTP/SAVPF (encrypted SRTP using DTLS-SRTP with RTCP feedback)"
Before making calls, also make sure that you give the caller and callee E164 Number values:
1. Go to "Settings > Subscriber", find the subscriber and click "Details"
1. Go to "Master Data" section and click "Edit"
1. Input E164 number (for example 43 12 3456) and click save
Troubleshooting tips for when the call feature does not want to enable:
1. Log out and in again of CSC
1. Check that rtcengine is active in your vagrant box, by executing as root:
`ngcp-service summary`
1. If inactive, restart it:
`ngcp-service rtcengine restart`
1. If still not working, try restarting the proxy
`ngcp-service proxy restart`
For the call feature to properly work, make sure you're logged out of ngcp-panel before logging in to CSC.
### PBX Customer
You need a pbx subscriber to be able to access the pbx config specific modules in ngcp-csc-ui as a user. To create a pbx subscriber we first need to enable pbx in vagrant.
1. SSH into the vagrant box and become root
`
./v s pro1
sudo -s
`
1. Edit config.yml
`vim /etc/ngcp-config/config.yml`
1. Enable csc by finding the "pbx:" section of the file, and setting "enable:" to yes
`enable: yes`
1. Enable write access for subscriberadmins by adding "write" to subscriber privileges as in the following example
```
www_admin
privileges:
subscriberadmin:
subscribers:
- write
```
1. Run ngcpcfg apply
`ngcpcfg apply 'Enable pbx'`
1. Log in to ngcp-panel with administrator credentials
1. Go to Settings > Customers
1. Look for customer row with "Cloud PBX Account" written in "Product" column, and click "Details" for that row
1. Go to Subscribers, and click "Create Subscribers". It's good to note that the first subscriber we create becomes the "pilot" for this customer
1. In the "Domains" section, select the row with your local vagrant ip address shown in the "Domain" column
1. For E.164 Number input a dummy number, for example 43 12 34567
1. Input dummy "Display Name", "Email", "Web Username/Password", and "Sip Username/Password", and make sure to select "Administrative", then press "Save"
1. Create another subscriber. This new subscriber (and all subsequent ones) will then becomes a normal pbx subscriber
1. For normal subscribers input an extension in addition to the other fields mentioned above (including "Administrative"), then press "Save"
Now you can log in to csc with one of the normal subscriber you just created. URL for login is the same as for accessing ngcp-panel admin, except with csc suffix and no port specified:
`https://<your-ip-address>/csc`
### Send Fax
You need to first enable faxserver and activate it for the subscriber, to be able to send a fax via the "action button menu".
1. By default, vagrant-ngcp has faxserver enabled by default in the config, so currently we do not need to make any changes here. Otherwise, it would be enabled via /etc/ngcp-config/config.yml by setting "faxserver: enable:" to "yes" and applying the changes with ngcpcfg apply 'enable faxserver'""
1. We need to set up the MTA (Mail Transfer Agent) exim4 so we can send the fax via mail. SSH in to the vagrant system and then execute ``sudoedit /etc/ngcp-config/config.yml` with the following configuration:
`email:
domain: ''
hostname: ''
smarthost:
hostname: 'mail.sipwise.com'
password: ''
reverse_hostnames: []
username: ''
`
1. Apply the exim configuration changes via `sudo ngcpcfg apply 'adjust exim4 / MTA configuration'`
1. Log in to ngcp-panel with administrator credentials
1. Go to "Settings > Subscribers", find subscriber you want to use as caller, and click "Details"
1. Under "Master Data" click edit, and enter subscribers number also in the E164 field
1. Then go to "Preferences", and set "Fax2Mail and Sendfax" to active
1. Repeat the two steps above, this time for the callee
1. For the callee, you also need to add your internal sipwise email address as "Destination" under "Fax2Mail and Sendfax", and also under "Call Forwards" configure a "Call Forward Unconditional" to "Fax2Mail"
1. Additionally, the visibility of the fax option in "action button menu" is reliant on store state "sendFax: true" in src/store/user.js. This means it can be toggled off in the code as well if needed
### How to add new npm package
1. Remove the package if you've already installed it
`npm remove <package> <--save-dev || --save>`
1. Ensure that you have a clean node_modules folder
`
rm -R node_modules/
npm install
`
1. Remove obsolete shrinkwrap file
`rm npm-shrinkwrap.json`
1. Install new package(s)
`npm install packageA packageB --save-dev`
1. Generate new shrinkwrap file including all dependencies
`npm shrinkwrap --dev`
You should see the following result in console:
`wrote npm-shrinkwrap.json`
1. Add new shrinkwrap file to git
`git add .`
## Contributing
See our [Contributing Guide](./CONTRIBUTING.md) file) for information on how to contribute.
`src/components/CscMainMenuTop.vue`
```javascript
{
to: '/user/new-feature',
icon: 'fancy_icon',
label: this.$t('navigation.newFeature.title'),
sublabel: this.$t('navigation.newFeature.subTitle'),
visible: true
}
```
### NGCP API
All API functions are located in `src/api`. The file `src/api/common.js`
exports basic convenient functions to perform API requests.
Check [API Documentation](https://dev-web-trunk.mgm.sipwise.com:1443/api) for further details.
### Authentication
The standard authentication method to access the API from the browser is the [JSON Web Token (JWT)](https://jwt.io) which is specified in [RFC7519](https://tools.ietf.org/html/rfc7519)
After the login request, the JWT is stored in the LocalStorage and is added automatically on each API request.
#### Fetch a list of items
```javascript
const list = await getList({
resource: 'subscribers'
})
list.items.forEach(subscriber => {
console.log(subscriber.webusername)
})
```
#### Fetch a paginated list of items
```javascript
const list = await getList({
resource: 'subscribers',
page: 1,
rows: 25
})
console.log(list.lastPage)
```
#### Fetch a single item
```javascript
const subscriber = await get({
resource: 'subscribers',
resourceId: 21
})
console.log(subscriber.webusername)
```
#### Create a new item
If you use `post`, you create the
item and get back the sanitised data.
The method `postMinimal` does exactly
the same, except that the body is empty.
```javascript
const subscriber = await post({
resource: 'subscribers',
body: {
webusername: 'alice',
...
}
})
console.log(subscriber.webusername)
```
```javascript
await postMinimal({
resource: 'subscribers',
body: {
webusername: 'alice',
...
}
})
```
#### Update an existing item
This method helps to update an entire item at once.
```javascript
const subscriber = await put({
resource: 'subscribers',
resourceId: 21,
body: {
webusername: 'bob',
...
}
})
console.log(subscriber.webusername)
```
```javascript
await putMinimal({
resource: 'subscribers',
resourceId: 21,
body: {
webusername: 'bob',
...
}
})
```
#### Update a specific field on an existing item
This is the preferred method to update single fields on an item.
```javascript
await patchReplace({
resource: 'subscribers',
resourceId: 21,
fieldPath: 'webusername',
value: 'carol'
})
```
```javascript
const subscriber = await patchReplaceFull({
resource: 'subscribers',
resourceId: 21,
fieldPath: 'webusername',
value: 'dave'
})
console.log(subscriber.webusername)

@ -9,13 +9,35 @@ export const LIST_DEFAULT_PAGE = 1
export const LIST_DEFAULT_ROWS = 25
export const LIST_ALL_ROWS = 1000
export const ContentType = {
json: 'application/json',
jsonPatch: 'application/json-patch+json'
}
export const Prefer = {
minimal: 'return=minimal',
representation: 'return=representation'
}
const PATCH_HEADERS = {
'Content-Type': 'application/json-patch+json',
Prefer: 'return=minimal'
'Content-Type': ContentType.jsonPatch,
Prefer: Prefer.minimal
}
const GET_HEADERS = {
Accept: 'application/json'
Accept: ContentType.json
}
const POST_HEADERS = {
Accept: ContentType.json,
'Content-Type': ContentType.json,
Prefer: Prefer.representation
}
const PUT_HEADERS = {
Accept: ContentType.json,
'Content-Type': ContentType.json,
Prefer: 'return=representation'
}
export class ApiResponseError extends Error {
@ -82,6 +104,16 @@ export async function getList (options) {
}
}
function handleResponseError (err) {
const code = _.get(err, 'body.code', null)
const message = _.get(err, 'body.message', null)
if (code !== null && message !== null) {
throw new ApiResponseError(err.body.code, err.body.message)
} else {
throw err
}
}
export async function get (options) {
options = options || {}
options = _.merge({
@ -108,13 +140,7 @@ export async function get (options) {
}
return body
} catch (err) {
const code = _.get(err, 'body.code', null)
const message = _.get(err, 'body.message', null)
if (code !== null && message !== null) {
throw new ApiResponseError(err.body.code, err.body.message)
} else {
throw err
}
handleResponseError(err)
}
}
@ -139,13 +165,7 @@ export async function patch (operation, options) {
headers: options.headers
})
} catch (err) {
const code = _.get(err, 'body.code', null)
const message = _.get(err, 'body.message', null)
if (code !== null && message !== null) {
throw new ApiResponseError(err.body.code, err.body.message)
} else {
throw err
}
handleResponseError(err)
}
}
@ -184,6 +204,72 @@ export function patchRemoveFull (options) {
return patchFull('remove', options)
}
export async function post (options) {
options = options || {}
options = _.merge({
headers: POST_HEADERS
}, options)
let path = options.path
if (options.resource !== undefined) {
path = 'api/' + options.resource + '/'
}
try {
const res = await Vue.http.post(path, options.body, {
headers: options.headers
})
if (options.headers.Prefer === Prefer.representation) {
return normalizeEntity(getJsonBody(res.body))
} else {
return null
}
} catch (err) {
handleResponseError(err)
}
}
export async function postMinimal (options) {
options = options || {}
options = _.merge(options, {
headers: {
Prefer: 'return=representation'
}
})
await post(options)
}
export async function put (options) {
options = options || {}
options = _.merge({
headers: PUT_HEADERS
}, options)
let path = options.path
if (options.resource !== undefined && options.resourceId !== undefined) {
path = 'api/' + options.resource + '/' + options.resourceId
}
try {
const res = await Vue.http.put(path, options.body, {
headers: options.headers
})
if (options.headers.Prefer === Prefer.representation) {
return normalizeEntity(getJsonBody(res.body))
} else {
return null
}
} catch (err) {
handleResponseError(err)
}
}
export async function putMinimal (options) {
options = options || {}
options = _.merge(options, {
headers: {
Prefer: 'return=representation'
}
})
await put(options)
}
export async function del (options) {
options = options || {}
options = _.merge({
@ -200,13 +286,7 @@ export async function del (options) {
try {
await Vue.http.delete(path, requestOptions)
} catch (err) {
const code = _.get(err, 'body.code', null)
const message = _.get(err, 'body.message', null)
if (code !== null && message !== null) {
throw new ApiResponseError(err.body.code, err.body.message)
} else {
throw err
}
handleResponseError(err)
}
}

Loading…
Cancel
Save