Add a custom service to the Remote Monitoring solution accelerator web UI

This article shows you how to add a new service into the Remote Monitoring solution accelerator web UI. The article describes:

  • How to prepare a local development environment.
  • How to add a new service to the web UI.

The example service in this article provides the data for a grid that the Add a custom grid to the Remote Monitoring solution accelerator web UI how-to article shows you how to add.

In a React application, a service typically interacts with a back-end service. Examples in the Remote Monitoring solution accelerator include services that interact with the IoT hub manager and configuration microservices.

Prerequisites

To complete the steps in this how-to guide, you need the following software installed on your local development machine:

Before you start

You should complete the steps in the Add a custom page to the Remote Monitoring solution accelerator web UI how-to article before continuing.

Add a service

To add a service to the web UI, you need to add the source files that define the service, and modify some existing files to make the web UI aware of the new service.

Add the new files that define the service

To get you started, the src/walkthrough/services folder contains the files that define a simple service:

exampleService.js


import { Observable } from 'rxjs';
import { toExampleItemModel, toExampleItemsModel } from './models';

/** Normally, you'll need to define the endpoint URL.
 * See app.config.js to add a new service URL.
 *
 * For this example, we'll just hardcode sample data to be returned instead
 * of making an actual service call. See the other service files for examples.
 */
//const ENDPOINT = Config.serviceUrls.example;

/** Contains methods for calling the example service */
export class ExampleService {

  /** Returns an example item */
  static getExampleItem(id) {
    return Observable.of(
      { ID: id, Description: "This is an example item." },
    )
      .map(toExampleItemModel);
  }

  /** Returns a list of example items */
  static getExampleItems() {
    return Observable.of(
      {
        items: [
          { ID: "123", Description: "This is item 123." },
          { ID: "188", Description: "This is item ONE-DOUBLE-EIGHT." },
          { ID: "210", Description: "This is item TWO-TEN." },
          { ID: "277", Description: "This is item 277." },
          { ID: "413", Description: "This is item FOUR-THIRTEEN." },
          { ID: "789", Description: "This is item 789." },
        ]
      }
    ).map(toExampleItemsModel);
  }

  /** Mimics a server call by adding a delay */
  static updateExampleItems() {
    return this.getExampleItems().delay(2000);
  }
}

To learn more about how services are implemented, see The introduction to Reactive Programming you've been missing.

model/exampleModels.js

import { camelCaseReshape, getItems } from 'utilities';

/**
 * Reshape the server side model to match what the UI wants.
 *
 * Left side is the name on the client side.
 * Right side is the name as it comes from the server (dot notation is supported).
 */
export const toExampleItemModel = (data = {}) => camelCaseReshape(data, {
  'id': 'id',
  'description': 'descr'
});

export const toExampleItemsModel = (response = {}) => getItems(response)
  .map(toExampleItemModel);

Copy exampleService.js to the src/services folder and copy exampleModels.js to the src/services/models folder.

Update the index.js file in the src/services folder to export the new service:

export * from './exampleService';

Update the index.js file in the src/services/models folder to export the new model:

export * from './exampleModels';

Set up the calls to the service from the store

To get you started, the src/walkthrough/store/reducers folder contains a sample reducer:

exampleReducer.js

import 'rxjs';
import { Observable } from 'rxjs';
import moment from 'moment';
import { schema, normalize } from 'normalizr';
import update from 'immutability-helper';
import { createSelector } from 'reselect';
import { ExampleService } from 'walkthrough/services';
import {
  createReducerScenario,
  createEpicScenario,
  errorPendingInitialState,
  pendingReducer,
  errorReducer,
  setPending,
  toActionCreator,
  getPending,
  getError
} from 'store/utilities';

// ========================= Epics - START
const handleError = fromAction => error =>
  Observable.of(redux.actions.registerError(fromAction.type, { error, fromAction }));

export const epics = createEpicScenario({
  /** Loads the example items */
  fetchExamples: {
    type: 'EXAMPLES_FETCH',
    epic: fromAction =>
      ExampleService.getExampleItems()
        .map(toActionCreator(redux.actions.updateExamples, fromAction))
        .catch(handleError(fromAction))
  }
});
// ========================= Epics - END

// ========================= Schemas - START
const itemSchema = new schema.Entity('examples');
const itemListSchema = new schema.Array(itemSchema);
// ========================= Schemas - END

// ========================= Reducers - START
const initialState = { ...errorPendingInitialState, entities: {}, items: [], lastUpdated: '' };

const updateExamplesReducer = (state, { payload, fromAction }) => {
  const { entities: { examples }, result } = normalize(payload, itemListSchema);
  return update(state, {
    entities: { $set: examples },
    items: { $set: result },
    lastUpdated: { $set: moment() },
    ...setPending(fromAction.type, false)
  });
};

/* Action types that cause a pending flag */
const fetchableTypes = [
  epics.actionTypes.fetchExamples
];

export const redux = createReducerScenario({
  updateExamples: { type: 'EXAMPLES_UPDATE', reducer: updateExamplesReducer },
  registerError: { type: 'EXAMPLE_REDUCER_ERROR', reducer: errorReducer },
  isFetching: { multiType: fetchableTypes, reducer: pendingReducer }
});

export const reducer = { examples: redux.getReducer(initialState) };
// ========================= Reducers - END

// ========================= Selectors - START
export const getExamplesReducer = state => state.examples;
export const getEntities = state => getExamplesReducer(state).entities || {};
export const getItems = state => getExamplesReducer(state).items || [];
export const getExamplesLastUpdated = state => getExamplesReducer(state).lastUpdated;
export const getExamplesError = state =>
  getError(getExamplesReducer(state), epics.actionTypes.fetchExamples);
export const getExamplesPendingStatus = state =>
  getPending(getExamplesReducer(state), epics.actionTypes.fetchExamples);
export const getExamples = createSelector(
  getEntities, getItems,
  (entities, items) => items.map(id => entities[id])
);
// ========================= Selectors - END

Copy exampleReducer.js to the src/store/reducers folder.

To learn more about the reducer and Epics, see redux-observable.

Configure the middleware

To configure the middleware, add the reducer to the rootReducer.js file in the src/store folder:

import { reducer as exampleReducer } from './reducers/exampleReducer';

const rootReducer = combineReducers({
  ...appReducer,
  ...devicesReducer,
  ...rulesReducer,
  ...simulationReducer,
  ...exampleReducer
});

Add the epics to the rootEpics.js file in the src/store folder:

import { epics as exampleEpics } from './reducers/exampleReducer';

// Extract the epic function from each property object
const epics = [
  ...appEpics.getEpics(),
  ...devicesEpics.getEpics(),
  ...rulesEpics.getEpics(),
  ...simulationEpics.getEpics(),
  ...exampleEpics.getEpics()
];

Next steps

In this article, you learned about the resources available to help you add or customize services in the web UI in the Remote Monitoring solution accelerator.

Now you have defined a service, the next step is to Add a custom grid to the Remote Monitoring solution accelerator web UI that displays data returned by the service.

For more conceptual information about the Remote Monitoring solution accelerator, see Remote Monitoring architecture.