8

Making a Dashboard

Most of the work done so far has focused on building microservices and making them interact with each other. It is time to bring humans into the equation, adding a User Interface (UI) through which our end users can use the system with a browser and change settings that may be awkward or unwise to do through Slack.

Modern web applications greatly rely on client-side JavaScript (JS, also known as ECMAScript). Some JS frameworks go all the way in terms of providing a full Model-View-Controller (MVC) system, which runs in the browser and manipulates the Document Object Model (DOM), the structured representation of the web page that is rendered in a browser.

The web development paradigm has shifted from rendering everything on the side of the server, to rendering everything on the client side with data collected from the server on demand. The reason is that modern web applications change portions of a loaded web page dynamically instead of calling the server for a full rendering. It is faster, requires less network bandwidth, and offers a richer user experience. Delays of a few seconds can cause a user to navigate away from your page, unless they have a strong need to visit it, such as, more specifically, a need to shop or read. One of the biggest examples of this client-side shift is the Gmail app, which pioneered these techniques around 2004.

Tools like Facebook's ReactJS (https://facebook.github.io/react/) provide high-level APIs to avoid manipulating the DOM directly and offer a level of abstraction which makes client-side web development as comfortable as building Quart applications.

That said, there seems to be a new JS framework every other week, and it is often hard to decide which one to use. AngularJS (https://angularjs.org/) used to be the coolest toy, but now many developers have switched to implementing most of their application UIs with ReactJS. There are also newer languages, such as Elm (https://elm-lang.org), which offers a functional programming language that compiles to JavaScript, allowing the compile-time detection of many common programming errors while also having a runtime that will work with any browser. In the future, no doubt, another new player will be popular.

This volatility is not a bad sign at all. It simply means much innovation is happening in the JavaScript and browser ecosystem. Features like service workers allow developers to run JS code in the background natively: https://developer.mozilla.org/en/docs/Web/API/Service_Worker_API.

WebAssembly (https://webassembly.org/), an extremely fast and safe sandboxed environment, allows developers to produce resource-intensive tools, such as 3D rendered environments, all running in a web browser.

If you have a clean separation between your UI and the rest of the system, moving from one JS framework to another should not be too hard. This means that you should not change how your microservices publish data to make them specific to a JS framework.

For our purposes, we shall use ReactJS to build our little dashboard, and we will wrap it in a dedicated Quart application that bridges it to the rest of the system. We will also see how that app can interact with all our microservices. We have chosen this approach due to ReactJS's current popularity, though you will also get excellent results in any of the other popular environments.

This chapter is composed of the following three parts:

  • Building a ReactJS dashboard—a short introduction to ReactJS with an example
  • How to embed ReactJS in a Quart app and structure the application
  • Authentication and authorization

By the end of this chapter, you should have a good understanding of how to build a web UI using Quart, with the knowledge of how to make it interact with microservices—whether you choose to use ReactJS or not.

Building a ReactJS dashboard

The ReactJS framework implements its abstraction of the DOM and provides fast and efficient machinery to support dynamic events. Creating a ReactJS UI involves creating classes with a few standard methods, which will get called when events happen, such as the DOM being ready, the React class having been loaded, or user input occurring.

In a similar way to a web server such as nginx, taking care of all the difficult and common parts of the network traffic and leaving you to deal with the logic in your endpoints, ReactJS lets you concentrate on the implementation in your methods instead of worrying about the state of the DOM and the browser. Implementing classes for React can be done in pure JavaScript, or using an extension called JSX. We will discuss JSX in the next section.

The JSX syntax

Representing XML markup in a programming language can be hard work. A seemingly simple approach might be to treat all the markup as strings and format the content as if it were a template, but this approach means that your code does not understand what all that markup means. The other extreme would be creating each markup element as an object and rendering it all to the text representation.

Instead, there is a better, hybrid model using a transpiler – a type of compiler that generates a different form of source code instead of a runnable program. The JSX syntax extension (https://facebook.github.io/jsx/) adds XML tags to JavaScript and can be transpiled into pure JavaScript, either in the browser or beforehand. JSX is promoted by the ReactJS community as the best way to write React apps.

In the following example, a <script> section contains a greeting variable whose value is an XML structure representing a div; this syntax is valid JSX. From there, the ReactDOM.render() function can render the greeting variable in the DOM at the id you specify:

    <!DOCTYPE html>
    <html>
 
    <head lang="en">
        <meta charset="UTF-8">
    </head>
 
    <body>
        <div id="content"></div>
        <script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
        <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
        <script src="https://unpkg.com/babel-standalone@6/babel.min.js" crossorigin></script>
        <script type="text/babel">
            var greeting = (
                <div>
                    Hello World
                </div>)
            ReactDOM.render(greeting, document.getElementById('content'));
        </script>
    </body>
    </html>

The two ReactJS scripts are part of the React distribution, and here we are using the development versions, which will provide more helpful error messages while we are still writing our code. Smaller, encoded versions—known as minified—are preferred for production use, as they use less network bandwidth and cache storage space. The babel.min.js file is part of the Babel distribution and needs to be loaded before the browser encounters any JSX syntax.

Babel (https://babeljs.io/) is a transpiler that can convert JSX to JS on the fly, among other available conversions. To use it, you simply need to mark a script as being of type text/babel.

The JSX syntax is the only specific syntax difference to know about React, as everything else is done with common JavaScript. From there, building a ReactJS application involves creating classes to render markup and respond to events, and these will be used to render the web pages.

Let's now look at the heart of ReactJS – components.

React components

ReactJS is based on the idea that a web page can be constructed from basic components, which are invoked to render different parts of the display and respond to events such as typing, clicks, and new data appearing.

As an example, if you want to display a list of people, you can create a Person class that is in charge of rendering a single person's information, given its values, and a People class that iterates through a list of people and calls the Person class to render each item.

Each class is created with the React.createClass() function, which receives a mapping containing the future class methods. The createClass() function generates a new class, and sets a props attribute to hold some properties alongside the provided methods. In the following example, in a new JavaScript file, we define a Person class with a render() function, which returns a <div> tag, and a People class which assembles the Person instances:

class Person extends React.Component {
    render() {
        return (
            <div>{this.props.name} ({this.props.email})</div>
        );
    }
}
class People extends React.Component {
    render() {
        var peopleNodes = this.props.data.map(function (person) {
            return (
                <Person
                    key={person.email}
                    name={person.name}
                    email={person.email}
                />
            );
        });
        return (
            <div>
                {peopleNodes}
            </div>
        );
    }
}

The Person class returns a div—a section or division—containing details about the person by referring to the props attribute in the instance. Updating these properties will update the object, and so update the display.

The props array is populated when the Person instance is created; this is what happens in the render() method of the People class. The peopleNodes variable iterates through the People.props.data list, which contains a list of the people we want to show. Each Person class is also provided with a unique key so that it can be referred to later if needed.

All that is left to do is instantiate a People class and put a list of people to be displayed by React in its props.data list. In our Jeeves app, this list can be provided by the appropriate microservice—the data service for information that we store, or a different service if we are fetching data from a third party. We can load the data asynchronously using an Asynchronous JavaScript and XML (AJAX) pattern using the built-in fetch method, or another helper library.

That is what happens in the loadPeopleFromServer() method in the following code, which builds on the previous example – add it to the same jsx file. The code calls our data service on the endpoint that lists all users, using a GET request and expecting some JSON as a response. Then, it sets the properties of the React component with the result, which propagates down through the other classes:

class PeopleBox extends React.Component {
    constructor(props) {
        super(props);
        this.state = { data: [] };
    }
    loadPeopleFromServer() {
        fetch('http://localhost:5000/api/users')
            .then(response => response.json())
            .then(data => {
                console.log(data);
                this.setState({
                    data: data,
                });
                console.log(this.state);
            })
            .catch(function (error) {
                console.log(error);
            });
    }
    componentDidMount() {
        this.loadPeopleFromServer();
    }
        render() {
            return (
                <div>
                    <h2>People</h2>
                    <People data={this.state.data} />
                </div>
            );
        }
 
    }
 
    const domContainer = document.querySelector('#people_list');
    ReactDOM.render(React.createElement(PeopleBox), domContainer);

When the state changes, an event is passed to the React class to update the DOM with the new data. The framework calls the render() method, which displays the <div> containing People. The People instance, in turn, passes data down to each Person instance in a cascade.

To trigger the loadPeopleFromServer() method, the class implements the componentDidMount() method, which gets called once the class instance is created and mounted in React, ready to be displayed. Last, but not least, the class's constructor provides an empty set of data so that in the time before anything has loaded, the display is not broken.

This whole process of decomposition and chaining may seem complicated at first but, once in place, it is powerful and straightforward to use: it allows you to focus on rendering each component and letting React deal with how to do it in the most efficient way in the browser.

Each component has a state, and when something changes, React first updates its own internal representation of the DOM—the virtual DOM. Once that virtual DOM is changed, React can apply the required changes efficiently on the actual DOM.

All the JSX code we've seen in this section can be saved in a JSX file – it's static content, so let's place it in a directory called static – and used in an HTML page as follows. There is also a small helper microservice to serve these files in the code samples at https://github.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/tree/main/CodeSamples.

    <!DOCTYPE html>
    <html>
 
    <head lang="en">
        <meta charset="UTF-8">
    </head>
 
    <body>
        <div class="container">
            <h1>Jeeves Dashboard</h1>
            <br>
            <div id="people_list"></div>
        </div>
 
        <script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
        <script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
        <script src="https://unpkg.com/babel-standalone@6/babel.min.js" crossorigin></script>
        <script src="/static/people.jsx" type="text/babel"></script>
        <script type="text/babel">
 
        </script>
 
    </body>
 
    </html>

The PeopleBox class is instantiated with the /api/users URL for this demonstration, and once the web page has loaded and been processed, the componentDidMount methods are triggered, React calls that URL, and it expects to get back a list of people, which it passes down the chain of components.

Notice that we have also set up where to render the components in the last two lines: first, we find the element in the HTML with the right identifier, and then tell React to render a class within it.

Using transpilation directly in the browser is unnecessary, as it can be done while building and releasing the application, as we will see in the next section.

This section described a very basic usage of the ReactJS library and did not dive into all its possibilities. If you want to get more info on React, you should try the tutorial at https://reactjs.org/tutorial/tutorial.html as your first step. This tutorial shows you how your React components can interact with the user through events, which is the next step once you know how to do some basic rendering.

Pre-processing JSX

So far, we have relied on the web browser to convert the JSX files for us. We could still do that, however, it will be the same work being done by each web browser that visits our site. Instead, we can process our own JSX files and provide pure JavaScript to people visiting our site. To do that we must install some tools.

Firstly, we need a JavaScript package manager. The most important one to use is npm (https://www.npmjs.com/). The npm package manager is installed via Node.js. On macOS, the brew install node command does the trick, or you can go to the Node.js home page (https://nodejs.org/en/) and download it to the system. Once Node.js and npm are installed, you should be able to call the npm command from the shell as follows:

$ npm -v 
7.7.6

Converting our JSX files is straightforward. Move the .jsx file we have created from static/ to a new directory called js-src. Our directory layout should now look like this:

  • mymicroservice/
    • templates/ – all of our html files
    • js-src/ – our jsx source code
    • static/ – the JavaScript results of the transpilation

We can then install the tools we need using:

$ npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/preset-react

Then, for our development, we can start a command that will continuously watch our js-src directory for any changes to the files, and automatically update them, in much the same way that the development version of Quart will reload Python files automatically. In a new terminal, type:

$ npx babel --watch js-src/ --out-dir static/  --presets @babel/preset-react

We can then see that it creates .js files for you and does so each time you save your changes to the JSX files in js-src/.

To deploy our application, we can either generate the JavaScript files and commit them into the repository or generate them as part of the CI process. In either case, the command to process the files once is remarkably similar—we just don't watch the directory, and we use the production presets:

$ npx babel js-src/ --out-dir static/ --presets @babel/preset-react

With all the changes, the final index.html file just needs a small change to use the .js file instead of the .jsx one:

        <script src="/static/people.js"></script> 

Now that we have the basic layout for building a React-based UI, let's see how we can embed it in our Quart world.

ReactJS and Quart

From the perspective of the server, the JavaScript code is a static file, and so serving React apps with Quart is no trouble at all. The HTML page can be rendered with Jinja2, and the transpiled JSX files can be provided as static content alongside it, much like you would do for plain JavaScript files. We can also get the React distribution and serve those files, or rely on a Content Delivery Network (CDN) to provide them.

In many cases a CDN is the better option, as retrieving the files will be faster, and the browser then has the option of recognizing that it has already downloaded these files and can use a cached copy to save time and bandwidth. Let's name our Quart application dashboard, and start off with a simple structure like this:

  • setup.py
  • dashboard/
    • __init__.py
    • app.py
    • templates/
    • index.html
    • static/
    • people.jsx

The basic Quart application that serves the unique HTML file will look like this:

    from quart import Quart, render_template 
    app = Quart(__name__) 
 
    @app.route('/') 
    def index(): 
        return render_template('index.html') 
 
    if __name__ == '__main__': 
        app.run() 

Thanks to Quart's convention on static assets, all the files contained inside the static/ directory are served under the /static URL. The index.html template looks like the one described in the previous section and can grow into something Quart-specific later on. That is all we need to serve a ReactJS-based app through Quart.

Throughout this section, we have worked on the assumption that the JSON data that React picked was served by the same Quart app. Doing AJAX calls on the same domain is not an issue, but in case you need to call a microservice that belongs to another domain, there are a few changes required on both the server and the client side.

Cross-origin resource sharing

Allowing client-side JavaScript to perform cross-domain requests is a potential security risk. If the JS code that's executed in the client page for your domain tries to request a resource from another domain that you don't own, it could potentially run malicious JS code and harm your users. This is why all browsers use the W3C standard for cross-origin resources (https://www.w3.org/TR/2020/SPSD-cors-20200602/) when a request is made. They ensure that the requests can only be made to the domain that served the page to us.

Beyond security, it is also a good way to prevent someone from using your bandwidth for their web app. For instance, if you provide a few font files on your website, you might not want another website to use them on their page and use your bandwidth without any control. However, there are legitimate use cases for wanting to share your resources with other domains, and you can set up rules on your service to allow other domains to reach your resources.

That is what Cross-Origin Resource Sharing (CORS) is all about. When the browser sends a request to your service, an Origin header is added, and you can control whether it is in the list of authorized domains. If not, the CORS protocol requires that you send back a few headers listing the allowed domains. There is also a preflight mechanism where the browser questions the endpoint via an OPTIONS call to know whether the request it wants to make is authorized and what capabilities the server has available. On the client side, you do not have to worry about setting up these mechanisms. The browser makes the decisions for you, depending on your requests.

On the server side, however, you need to make sure your endpoints answer the OPTIONS calls, and you need to decide which domains can reach your resources. If your service is public, you can authorize all domains with a wildcard. However, for a microservice-based application where you control the client side, you should restrict the domains. The Quart-CORS (https://gitlab.com/pgjones/quart-cors/) project allows us to add support for this very simply:

    # quart_cors_example.py
    from quart import Quart
    from quart_cors import cors
    app = Quart(__name__)
    app = cors(app, allow_origin="https://quart.com")
    @app.route("/api")
    async def my_microservice():
        return {"Hello": "World!"}

When running this app and using curl to do a GET request, we can see the results in the Access-Control-Allow-Origin: * header:

$ curl -H "Origin: https://quart.com" -vvv http://127.0.0.1:5000/api
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5200 (#0)
> GET /api HTTP/1.1
> Host: 127.0.0.1:5000
> User-Agent: curl/7.64.1
> Accept: */*
> Origin: https://quart.com
>
< HTTP/1.1 200
< content-type: application/json
< content-length: 18
< access-control-allow-origin: quart.com
< access-control-expose-headers:
< vary: Origin
< date: Sat, 10 Apr 2021 18:20:32 GMT
< server: hypercorn-h11
<
* Connection #0 to host 127.0.0.1 left intact
{"Hello":"World!"}* Closing connection 0

Quart-CORS allows finer-grained permissions, with decorators that allow protecting a single resource or blueprint instead of the whole app, or limiting methods to GET, POST, or others. It's also possible to set configuration using environment variables, which helps the app remain flexible and get the correct settings at runtime.

For an in-depth understanding of CORS, the MDN page is a great resource that can be found here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. In this section, we have looked at how to set up CORS headers in our services to allow cross-domain calls, which are useful in JS apps. What's still missing to make our JS app fully functional is authentication and authorization.

Authentication and authorization

The React dashboard needs to be able to authenticate its users and perform authorized calls on some microservices. It also needs to enable the user to grant access to any third-party sites we support, such as Strava or GitHub.

We assume that the dashboard only works when you are authenticated and that there are two kinds of users: first-time and returning. The following is the user story for first-time users:

As a first-time user, when I visit the dashboard, there's a "login" link. When I click on it, the dashboard redirects me to Slack to grant access to my resources. Slack then redirects me back to the dashboard, and I am connected. The dashboard then starts to fill with my data.

As described, our Quart app performs an OAuth2 conversation with Slack to authenticate users—and we know that since we are setting up a Slack bot, people should already have an account there. Connecting to Slack also means we need to store the access token in the user profile so we can use it to fetch data later on.

Before going further, we need to make a design decision: do we want the dashboard merged with the dataservice, or do we want to have two separate apps?

A note about Micro Frontends

Now we are discussing authenticating our users with a web frontend, there is the question of where we should put the corresponding code. One recent trend in frontend architecture is the idea of Micro Frontends. Facing many of the same scaling and interoperability troubles as the backend has faced, some organizations are shifting towards small, self-contained user interface components that can be included in a larger site.

Let's imagine a shopping website. When you visit the front page, there will be several different parts, including:

  • Shopping categories
  • Site-wide news and events, such as upcoming sales
  • Highlighted and promoted items for sale, including customized recommendations
  • A list of items you have recently viewed
  • A widget allowing you to sign in or register for an account, plus other administrative tools

If we develop a single web page to deal with all of these elements, it can become large and complex very quickly, especially if we need to repeat elements across different pages on the site. With many sites, these different features are kept separate by separating out the <div> tags that anchor them in the page, and keeping the code in separate JavaScript files—whether or not those files are separate by the time they are loaded into the web page, as they have likely been compiled and minified.

This approach introduces some of the same complications that a monolithic backend suffers from. A change to any of the backend or its user interface means updating a microservice and the user interface elements that query it, and those may well be in different source control repositories or managed by different teams. Perhaps support for both the old and new ways has to be introduced for a managed migration, or careful timing with different deployment mechanisms has to happen.

By using a Micro Frontend architecture, these UI features can all be the responsibility of different teams and services. If the "recommendations" feature suddenly requires a new backend or a different JavaScript framework, that is possible, as the main site only knows about it as a self-contained feature to be included. Any change can also be self-contained, as the Micro Frontend UI components for the recommendations engine would live in the same repository and be provided by the same service. As long as the technique to include the Micro Frontend component doesn't change, the main user interface doesn't need to be changed; changes can be controlled entirely through the microservice it depends on.

This also frees up the people working on each component, as they can release new features and bug fixes on their own schedule without large cross-team coordination to deploy updated features in multiple areas. The teams just have to ensure their UI is included in a consistent manner, accepts the same data, such as a customer identifier, and returns a UI element of the desired size.

Let's look at the Packt website as an example. When loading the main web page, we get to see a banner along the top containing the usual options we expect, a banner below for current promotions and events, and then a listing of recently added stock to be brought to the reader's attention:

Figure 8.1: Packt home page and its constituent parts

If we were designing this page, we could construct at least three different micro-frontends: an authentication component that handles sessions and logging in, an events component that can display and react to upcoming conferences and promotions, and an inventory component that can display current stock. This approach isn't ideal for all situations; on many occasions, a user interface needs to interact closely with other elements, or perhaps the spread of knowledge within an organization doesn't allow it to produce many small user interface components this way.

It is also worth noting that this architecture does not require many different URLs. The same nginx load balancer could be configured to route different URLs to different backend services without the client being any the wiser—and this may give a useful approach to migrating to such an architecture, as it lowers the chance of you needing to update the endpoint URLs.

With all that said, the Micro Frontend model is still relatively new, and many best practices and even bits of terminology are still in flux. For that reason, we shall focus on a simpler variant of this approach and have the authentication service provide its own HTML for logging a user in and creating an account that can be included in an iframe within another page if desired.

Getting the Slack token

Slack provides a typical three-legged OAuth2 implementation, using a simple set of HTTP GET requests. Implementing the exchange is done by redirecting the user to Slack and exposing an endpoint the user's browser is redirected to once access has been granted.

If we request the special identify scope, then all we get from Slack is confirmation of the user's identity and the unique Slack ID string. We can store all this information in the Quart session, use it as our login mechanism, and pass the e-mail and token values to DataService for use with other components if we need to.

As we did in Chapter 4, Designing Jeeves, let us implement the function that generates the URL to send the user to, combined with the other information Slack needs, which is documented at https://api.slack.com/legacy/oauth:

    @login.route("/login/slack")
    async def slack_login():
        query = {
            "client_id": current_app.config["JEEVES_CLIENT_ID"],
            "scope": "identify",
            "redirect_uri": current_app.config["SLACK_REDIRECT_URI"],
        }
        url = f"https://slack.com/oauth/authorize?{urlencode(query)}"
        return redirect(url)

Here, we are running our Quart application behind nginx with a Let's Encrypt certificate, as we also set up in Chapter 4, Designing Jeeves. This is why we are using a callback URL from our configuration rather than attempting to work it out dynamically, as that URL is tied to nginx.

That function uses the client_id from the Jeeves application generated in Slack and returns a redirection URL we can present to the user. The dashboard view can be changed accordingly to pass that URL to the template.

    @login.route("/")
    async def index():
        return await render_template("index.html", user=session.get("user"))

We also pass a user variable if there are any stored in the session. The template can then use the Strava URL to display a login/logout link as follows:

    {% if not user %}
    <a href="{{url_for('login.slack_login')}}">Login via Slack</a>
    {% else %}
    Hi {{user}}!
    <a href="/logout">Logout</a>
    {% endif %}

When the user clicks on the login link, they are redirected to Strava and back to our application on the endpoint, defined as SLACK_REDIRECT_URI. The implementation of that view could be like this:

    @login.route("/slack/callback")
    async def slack_callback():
        query = {
            "code": request.args.get("code"),
            "client_id": current_app.config["JEEVES_CLIENT_ID"],
            "client_secret": current_app.config["JEEVES_CLIENT_SECRET"],
            "redirect_uri": current_app.config["SLACK_REDIRECT_URI"],
        }
        url = "https://slack.com/api/oauth.access"
        response = requests.get(url, params=query)
        response_data = response.json()
        session["user"] = response_data["user_id"]
        return redirect(url_for("login.index"))

Using the response we get from Slack's OAuth2 service, we put the temporary code received into a query to convert that into a real access token. Then we can store the token in the session or send it to the data service.

We are not detailing how Dashboard interacts with TokenDealer, since we have already shown this in Chapter 7, Securing Your Services. The process is similar—the Dashboard app gets a token from TokenDealer and uses it to access DataService.

The last part of authentication is in the ReactJS code, as we will see in the next section.

JavaScript authentication

When the Dashboard app performs the OAuth2 exchange with Slack, it stores user information in the session, which is a fine approach for the user authenticating on the dashboard. However, when the ReactJS UI calls the DataService microservice to display the user runs, we need to provide an authentication header. The following are two ways to handle this problem:

  • Proxy all the calls to the microservices via the Dashboard web app using the existing session information.
  • Generate a JWT token for the end user, which can be stored and used against another microservice.

The proxy solution seems simplest as it removes the need to generate one token per user for accessing DataService, although that does mean that if we want to trace a transaction back to an individual user, we have to connect the DataService event to the frontend's list of events.

Proxying allows us to hide the DataService from public view. Hiding everything behind the dashboard means we have more flexibility to change the internals while keeping the UI compatible. The problem then is that we are forcing all the traffic through the Dashboard service even when it is not needed. Our exposed API and the Dashboard appear, to the end user, to have different routes to the data, which may cause confusion. It also means that if there is an outage with the DataService, then the Dashboard is affected and may stop responding to any people trying to view the page. If the JavaScript contacts the DataService directly, then the Dashboard will continue operating, and notifications can be put up to let people know there is an ongoing problem.

This leads us strongly towards the second solution, generating a token for the end user for use in the React frontend. If we are already dealing tokens to the other microservices, the web user interface is just one of the clients. However, this also means that the client has a second authentication loop, as it must first authenticate using OAuth2, and then fetch a JWT token for internal services.

As we discussed in the last chapter, we can generate a JWT token once we have authenticated, and then use that to communicate with the other services under our control. The workflow is exactly the same—it simply gets called from JavaScript.

Summary

In this chapter, we looked at the fundamentals of building a ReactJS UI dashboard served by a Quart application. ReactJS is an excellent way to build a modern interactive UI in the browser, as it introduces a new syntax called JSX which speeds up JS execution. We also looked at how to use a toolchain, based on npm, and Babel, to manage JS dependencies and transpile JSX files into pure JavaScript.

The Dashboard application uses Slack's OAuth2 API to connect users and authenticate them with our own service. We made the design decision to separate the Dashboard application from DataService, so the token is sent to the DataService microservice for storage. That token can then be used by the periodic workers as well as the Jeeves actions to perform tasks on behalf of the user.

Lastly, the calls made to different services to build the dashboard are made independently of the dashboard, allowing us to focus on doing one thing well in each component. Our authorization service deals with all the token generation, and our dashboard can focus on being responsive to the viewer.

Figure 8.2 contains a diagram of the new architecture, which includes the Dashboard app:

Figure 8.2: The full Jeeves microservice architecture

You can find the full code of the Dashboard in the PythonMicroservices organization on GitHub at https://github.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/.

With several different Quart apps that compose it, developing an application like Jeeves can be a challenge when you are a developer. In the next chapter, we will look at packaging and running the application to make maintenance and upgrading much easier.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset