React and the Report and Dashboard Server

I recently posted about some news in v18.1 of our Report and Dashboard Server and it made me curious to see how an interface could show a report without any ASP.NET components. We have several demos available that show the use of JavaScript applications as report viewing or editing front-ends (such as this one for React), but they assume that there’s a .NET based back-end, usually using ASP.NET MVC. The associated instructions also use bower, which isn’t something I would do today when creating a React app.

I set myself the goal to create a React app using create-react-app (duh!) and implement access to Report and Dashboard Server to show a report in the app, using the HTML5 Document Viewer. The basic steps of a solution are outlined on this documentation page, but again assume ASP.NET MVC.

The post below describes the steps I took in enough detail for you to follow along if you’re interested. Please let me know if you find anything missing or inadequately explained!

React and the DevExpress Report and Dashboard Server

Preparation

After setting up Report and Dashboard Server in a virtual machine, I configured the system with a Data Model and I created a report. I also set up email sending for the server and created a user account, which is necessary so I can authenticate the remote viewer using that account. Following the instructions I linked in the previous paragraph, I activated CORS.

Now I set out to create a first piece of code to run on Node.js, which contacts the server, retrieves a security token and then a list of available reports. Here’s what I ended up with:

const base = 'http://192.168.1.234:83';
const data = r => r.data;

axios
  .post(
    base + '/oauth/token',
    qs.stringify({
      username: 'sturm',
      password: 'secret',
      grant_type: 'password'
    }),
    {
      headers: { 'content-type': 'application/x-www-form-urlencoded' }
    }
  )
  .then(data)
  .then(d => d.access_token)
  .then(token => {
    console.log('Token: ', token);
    return axios
      .get(base + '/api/documents', {
        headers: { Authorization: 'Bearer ' + token }
      })
      .then(data)
      .then(d => {
        console.dir(d);
      });
  })
  .catch(err => {
    console.error(err);
  });

From the top, here are the steps of the process. First, I have an address and a port for the server, which depend on the server configuration. Port 83 is the default for a new installation and I went with that. You can see that I’m using HTTP as the protocol — more about that below.

Using axios (because I like Promises), I make a POST request to the server, using the path /oauth/token. The information I send is URL-encoded and I achieve this using the qs library. There are other solutions for this, but qs is easy to use and works on Node as well as in the browser. As a result of this call, I receive a Bearer Token from the server, which I need to use in further calls to prove authorization.

Note that the token expires sooner or later — mine were valid for twenty minutes each — so you should make sure to retrieve a new one close enough to the actual call you’re going to make!

To retrieve the list of available reports, I make a GET request to /api/documents and I pass the token as part of the Authorization header. This is of course a standard OAuth mechanism. The result is a list of available reports:

[
  {
    id: 1,
    categoryId: 1,
    modifiedById: 0,
    name: 'Venues',
    description: '',
    modifiedBy: 'DESKTOP-TMVPFDM\\sturm',
    modifiedWhen: '2018-05-23T10:08:06.867',
    documentType: 'Report',
    allowModify: false,
    allowDelete: false,
    optimisticLock: 0
  }
],

That was simple enough! There’s just one important thing to sort out…

HTTP - or what?

By default, the OAuth token can not be retrieved from the server using HTTP, it requires HTTPS. There is a good reason: if a third party should get hold of the token, they would be able to access the server using my credentials!

Since I’m running Report and Dashboard Server in a local virtual machine, I can’t easily get hold of a “real” (i.e. not a self-signed) SSL certificate. IIS will gladly provide a self-signed certificate for development purposes, but this is not accepted by a client without some extra steps. On Node, I can tell the client to ignore validation errors by using an HTTPS Agent:

const agent = new https.Agent({
  host: '192.168.1.234',
  port: 443,
  path: '/',
  rejectUnauthorized: false
});

However, this agent mechanism is not available in the browser, since browsers are not allowed to ignore certificate validation issues without direct user confirmation. This means that a self-signed certificate is useless for AJAX calls, even during development.

For my development and test scenario, I chose to deactivate the HTTPS requirement for OAuth. However, before I tell you how that works, you must understand that this is NEVER an acceptable solution for a real-world server outside a dev environment! I recommend you get a signed certificate for your server, for example using Windows ACME Simple in conjunction with Let’s Encrypt.

Now, here’s how to allow Report and Dashboard Server to use HTTP for OAuth, purely for development purposes and entirely at your own risk. Find the table GlobalSettings in the server database (by default called DevExpressReportServer). In that table, find the record with the Key value OAuthServerAllowInsecureHttp and set the associated Value to True. Restart the service using IIS Manager and you’ll be able to run code like mine above.

The React app

With all the preparation and research out of the way, I created a new React app:

create-react-app report-server-react

Since I never see the point in using yarn, I switched things to npm and made sure everything was installed correctly:

cd report-server-react
rm -rf yarn.lock node_modules
npm install

Now I installed required packages, starting with the ones to support the communication logic already shown above:

npm install --save axios qs

I also need the packages for the DevExpress functionality, plus globalize and cldr to support internationalization:

npm install --save cldr globalize devextreme devexpress-reporting

Finally, I need a special loader for webpack:

npm install --save-dev html-loader

Now for some code

I edited the file src/App.js and removed the default component implementation and the logo import. I added some import lines to pull in all the required library functionality:

import axios from 'axios';
import qs from 'qs';
import ko from 'knockout';
import 'devextreme/dist/css/dx.common.css';
import 'devextreme/dist/css/dx.light.css';
import 'devexpress-reporting/css/web-document-viewer-light.min.css';

const viewerHtml = require('devexpress-reporting/dx-web-document-viewer').Html;

I added a new App class:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { reportName: 'report/1' };
  }
  render() {
    return <ReportViewer reportUrl={this.state.reportName} />;
  }
}

Like in some of our existing examples, the value passed to the reportUrl attribute is retrieved from state. In the demo, I’m setting the value statically to report/<id> (that’s the id from the report list). Of course it is easy to imagine that the value would be retrieved from some other source in a real application.

Finally, here is the ReportViewer component. Again, this is based on our other examples. It is possible to separate the token retrieval logic, but I left it in place here to keep the structure simple.

class ReportViewer extends React.Component {
  constructor(props) {
    super(props);
    this.reportUrl = ko.observable(props.reportUrl);
  }
  render() {
    return (
      <div>
        <div ref="innerScript" />
        <div className="fullscreen" data-bind="dxReportViewer: $data" />
      </div>
    );
  }
  componentWillReceiveProps(newProps) {
    this.reportUrl(newProps.reportUrl);
  }
  componentDidMount() {
    this.refs.innerScript.innerHTML = viewerHtml;
    const baseUrl = 'http://192.168.1.234:83';
    axios
      .post(
        baseUrl + '/oauth/token',
        qs.stringify({
          username: 'sturm',
          password: 'secret',
          grant_type: 'password'
        }),
        {
          headers: { 'content-type': 'application/x-www-form-urlencoded' }
        }
      )
      .then(r => r.data)
      .then(d => d.access_token)
      .then(token => {
        ko.applyBindings(
          {
            reportUrl: this.reportUrl,
            remoteSettings: {
              serverUri: baseUrl,
              authToken: token
            }
          },
          this.refs.viewer
        );
      });
  }
  componentWillUnmount() {
    ko.cleanNode(this.refs.viewer);
  }
}

Once more, from the top, here are the important details of the implementation. First, in the constructor, there is a call to Knockout. This library is used by the document viewer internally, so I need to interface with it from my code. I create an observable value, initializing it with the reportUrl from my own props.

In the render method, two container divs are created. The second one has a data-bind attribute, which is later recognized by Knockout. Since the attribute uses the dxReportViewer binding type, Knockout will integrate the viewer in place of the container component.

In componentWillReceiveProps, I update the internal observable value if and when the same value in the props changes. This in turn updates the viewer since Knockout reacts to the change to the observable value.

The method componentDidMount is called when the React component is ready, and you’ll recognize most of the code in there. The new part is the call to ko.applyBindings: at this point, Knockout receives a data model, which in this case is a structure with document viewer parameters. These are used by the data binding described above, since there is the $data variable in the attribute.

The only remaining element is a bit of clean-up logic in componentWillUnmount.

One final code addition was required: I edited the file src/App.css and added this block at the end, so that the viewer appears in a full-screen setup:

.fullscreen {
  position: absolute;
  top: 0px;
  left: 0;
  right: 0;
  bottom: 20px;
}

And then… nothing worked

With all the code in place, it should have been possible to run the application at this point. Since I used create-react-app, everything was set up for a simple command:

npm start

However, there were errors reported when I did this. The reason was that the globalize and cldr libraries are not compatible with webpack by default — and webpack is used by the create-react-app infrastructure, even if it’s almost invisible.

To solve this problem (and one more, below) it is necessary to modify the hidden webpack configuration that has been generated by create-react-app. Unfortunately this is not supported in the default setup. In fact, the makers of create-react-app advise that whenever you feel the need to break out of the standard system, you should let them know why, so they can fix the original issue rather than support customization.

Note: The project react-app-rewired aims to provide customizability of projects created using create-react-app, at a tradeoff. For real-world scenarios I recommend considering this seriously, for the demo however I didn’t want to complicate matters further by bringing in additional tools.

There is a mechanism in place for the purpose of breaking out, and it’s called eject. Once you eject your project, you are free to modify details like the webpack config files, but you will not be able to use your project with new versions of create-react-app and react-scripts in the future. There are lots of discussions to be found online on the pros and cons of ejecting. For the purposes of this demo, I chose to do it:

npm run eject

Note: If you’re following along with my description, please click the survey link displayed at the end of the eject process and let the project maintainers know that you ejected due to lack of support for CLDR, Globalize and custom loaders.

Adjusting webpack config files

I edited the file config/webpack.config.dev.js and found the block resolve.alias. It contained an item called react-native by default, and I added these lines to solve the integration problems that came up for CLDR and Globalize.

cldr$: 'cldrjs',
cldr: 'cldrjs/dist/cldr',
globalize: 'globalize/dist/globalize'

I made one more change by integrating the html-loader you saw already when I added the package. This was necessary because the devexpress-reporting package contains two files with HTML snippets, and these need to be supported by the loader. I found the block modules.rules and navigated towards the end of it. There is a segment there for the configuration of the file-loader, with a comment right behind it indicating that any custom loaders need to be added ahead of that block. I inserted my setup code in the correct position:

{
  test: [/\.html$/],
  include: [
    path.resolve(paths.appNodeModules, 'devexpress-reporting/html')
  ],
  loader: require.resolve('html-loader')
},

This configuration means that for any HTML files located in the devexpress-reporting package folder, the html-loader is used.

If you intend you build a production version of your project (npm run build), you will need to make the same changes to the file config/webpack.config.prod.js.

That’s it!

If you have been following along, all that remains at this point is to run the application:

npm start

This opens a page in a browser automatically, at http://localhost:3000, showing the running application. If all goes well, it will contact the Report and Dashboard Server, retrieve the report and show it in the HTML5 Document Viewer!

I’m really interested in your thoughts. Will you use this type of integration in your own applications? Please feel free to get back to us if you need more help with your own integration scenarios!

Finally, here is a repository with my own demo app. If you change the URL strings to point to your own server, this should work fine!

no comments
No Comments

Please login or register to post comments.