Skip to content

Creating a new module

Jakub Neruda edited this page Feb 15, 2018 · 24 revisions

Table of Contents

  1. Introduction
  2. Generating Core Files
    1. Angular Core
    2. Frontend Core
    3. Backend Core
    4. Services 101
  3. Summary

Introduction

This guide will walk you through the process of creating a new module for the Liberouter GUI (lgui) system. It will show you basics of setting up both frontend user part as well as backend server part that provides our system with data. Even though we will do all of our work within modules/ directly, you should create a new git repository which you will clone to the modules folder afterwards.

This guide is not intended to give details on module configuration or lgui deployment. Also make sure you meet the requiremens for the lgui, couple of those tools are used early in the development.

Generating Core Files

Angular Core

Whole lgui system uses Angular for frontend part and Python with Flask for the backend part. To ease the development as much as possible, we'll generate a couple of extra files that are needed to use angular console.

First navigate to modules/ and run following command:

ng new example --minimal --skip-commit --skip-git

This will create a new folder called example that will be properly set up as an Angular2 app. This is necessary to use the ng console to automatically generate some files. You might want to exclude --skip-commit --skip-git parameters when you will making a full-blown module, I am using them only for the sake of this guide.

The first thing we must do is to edit example/.angular-cli.json. Locate two lines that say:

"inlineStyle": true,
"inlineTemplate": true

and change both values to false. This will force ng console to generate separate files for HTML templates and CSS(scss) styles. More on how .angular-cli.json and package.json can be used is detailed in module configuration guide.

Frontend Core

When you'll navigate to example/src/app, you'll find two Typecript files. You would use this in your normal Angular app, but in lgui, things are a little bit more complicated. Create a folder called example once again. All frontend related source codes will be placed inside that folder.

Lgui has two major dependencies important for you. Bootstrap4 and ng-bootstrap. Lgui also creates links and buttons to your modules (once it is registered). You only need to provide it with some basic module description and everything will be fine. All of this is done with a module.ts file, which should look like this:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule} from '@angular/router';
import { FormsModule } from '@angular/forms';

import { NgbModule } from '@ng-bootstrap/ng-bootstrap';

import { AuthGuard } from 'app/utils/auth.guard';
import { SafePipe, SafePipeModule } from 'app/utils/safe.pipe';

const routes: Routes = [{
    path: 'example',
    component: ExampleComponent, // NOTE: Edit this
    canActivate: [AuthGuard],
    data: {
        role: 10,
        name: 'Example',
        description: 'dsc',
        icon: 'fa-internet-explorer',
    },
    children: []
}]

@NgModule({
    imports: [
        CommonModule,
        FormsModule,
        SafePipeModule,
        RouterModule.forChild(routes),
        NgbModule,
    ],
    declarations: [
    ],
    providers: [
        SafePipe
    ]
})
export class ExampleModule {}

Save this file as example.module.ts within src/app/example. Now please note one thing. In our guide, our module is called example. Entry point of our app is thus called ExampleModule (last line in the file). But we also need to register route for our module by which it will be integrated into lgui. This route is set to example (http://localhost:4200/example) and entry component for that route is called ExampleComponent. We will create that component in the moment. Just make sure you always follow the angular naming conventions.

Angular naming conventions: for class names, use camel toe notation with first letter in uppercase (ex.: MyGreatModule). If you name a file after your class, always use lowercase and separate words with dashes (ex.: my-great-module).

NOTE: Please note the path key in the Routes object. What you enter here will be viable for the configuration of the module.

Now we'll generate the codes for ExampleComponent with command (make sure you are in src/app/example:

ng g component example --flat

This command creates a three files - example.component.html, example.component.scss, example.component.ts. These are created within the same directory as is example.module.ts and that file is updated to link this new component. The --flat parameter forces creation in the same directory. Omit this parameter for any future components you'll make as they would be subordinates to the ExampleComponent (unless you are experienced enough to know what you are doing).

At this point we could stop, create module config file, bootstrap it and run the app. But for the sake of the completeness, we will now focus on the backend part of the module and on the frontend<->backend communication.

Backend Core

Return to root of our module (modules/example/). Create a new folder. Call it whatever you like, I prefer to call it simply backend as it will be root for all python backend codes.

Lgui backend uses Python+Flask for all the network related stuff. If you don't know how Flask works, google some tutorials, but here comes super simple explanation:

You register URL routes that should be recognized by the Flask server and define set of HTTP methods that should be supported for that routes. You also define which of your own python methods will be executed when somebody requests that route with the correct HTTP method. In the end, your python method must create a response, either ApiException (lgui specific) or some relevant data for your application (use JSON objects).

We will create two core files for the backend part - __init__.py which contains route registration and base.py which contains all methods that can be executed by visiting the routes. Starting with __init__.py, it should look like this:

from liberouterapi.modules.module import Module

# Register a blueprint
module_bp = Module('example', __name__, url_prefix = '/example', no_version=True)

from .base import *

module_bp.add_url_rule('/unprotected_data', view_func = unprotected_data, methods=['GET'])
module_bp.add_url_rule('/protected_data', view_func = protected_data, methods=['GET'])

As you can see, we had registered new module called example, residing on route /example. Then we had registered two url routes: /example/unprotected_data and /example/protected_data that will respond to any GET request. The protected route will drop any unauthorized connection.

Now we can mode to the base.py:

from liberouterapi import auth

import json

def unprotected_data():
    return json.dumps({'data': 'hello_world'})

@auth.required()
def protected_data():
    return json.dumps({'data': 'hello_user'})

As you can see, these methods don't do much. They only send back a json response with a single exception. unprotected_data() returns data always whereas protected_data() will return data only when http requrest contains login credentials. These credentials can be of anybody - guest, user or admin. More on roles and authorizations can be found here.

This is all for backend that is required by this guide. As a last step we have to somehow send the http request from frontend to backend and wait for the response. Now ho we'll do this?

Services 101

Javascript users are probably familiar with AJAX requests. Services in Angular2 are more or less an updated version of that concept. Plus, with lgui in play, all the user related stuff is done in the background, disconnected from you so you don't have to worry about such things.

Return to src/app/example and create new file example.service.ts and put following code in:

import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions, Response, URLSearchParams  } from '@angular/http';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class ExampleService {
    constructor (private http: Http) {}

    getUdata() {
        return this.http.get('/example/unprotected_data').map(
            (response: Response) => {
                return response.json();
            }).catch(this.handleError);
    }

    getPdata() {
        return this.http.get('/example/protected_data').map(
            (response: Response) => {
                return response.json();
            }).catch(this.handleError);
    }

    private handleError(err: Response | any) {
        return Promise.reject(err);
    }
}

The important bits are two functions requesting two routes registered by __init__.py. But this file does nothing on itself. It has to be imported into a component and used there. Open example.component.ts and make following edits:

import { Component, OnInit } from '@angular/core';

import { ExampleService } from './example.service'; // <-- First edit

@Component({
    selector: 'app-example',
    templateUrl: './example.component.html',
    styleUrls: ['./example.component.css'],
    providers: [ExampleService] // <-- Second edit
})
export class ExampleComponent implements OnInit {
    constructor(private api: ExampleService) { } // <-- Third edit
    ngOnInit() {}
}

When it is imported, we can modify ExampleComponent class to actually request data and store the result:

export class ExampleComponent implements OnInit {
    udata = null;
    pdata = null;
    error = null;

    constructor(private api: ExampleService) { }

    ngOnInit() {
        this.api.getUdata().subscribe(
            (data: Object) => this.processUData(data),
            (error: Object) => this.processError(error)
        );
        
        this.api.getPdata().subscribe(
            (data: Object) => this.processPData(data),
            (error: Object) => this.processError(error)
        );
    }

    processUData(data: any) {
        this.udata = data['data'];
    }

    processPData(data: any) {
        this.pdata = data['data'];
    }

    processError(error: any) {
        if (error['status'] >= 404) {
            this.error = error;
        }

        console.error('Error when retrieving data:');
        console.error(error);
    }
}

And as a last thing to do, we should edit the template html file to display the data. Edit example.component.html:

<h1>Example module!</h1>
<div>
    <div>
        {{ udata }}
    </div>
    <hr>
    <div>
        {{ pdata }}
    </div>
</div>

Summary

With this, your module almost ready to be deployed! But there is still one more thing. You have to make its configuration file. So go ahead and read this guide on the subject.