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

This article shows you how to add a new grid onto a page in the Remote Monitoring solution accelerator web UI. The article describes:

  • How to prepare a local development environment.
  • How to add a new grid to a page in the web UI.

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

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 following articles before continuing:

Add a grid

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

Add the new files that define the grid

To get you started, the src/walkthrough/components/pages/pageWithGrid/exampleGrid folder contains the files that define a grid:

exampleGrid.js


import React, { Component } from 'react';
import { Btn, ComponentArray, PcsGrid } from 'components/shared';
import { exampleColumnDefs, defaultExampleGridProps } from './exampleGridConfig';
import { isFunc, svgs, translateColumnDefs } from 'utilities';
import { checkboxColumn } from 'components/shared/pcsGrid/pcsGridConfig';

const initialState = {
  softSelectedId: undefined
};

/**
 * A grid for displaying example data
 *
 * Encapsulates the PcsGrid props
 */
export class ExampleGrid extends Component {
  constructor(props) {
    super(props);

    // Set the initial state
    this.state = initialState;

    // Default device grid columns
    this.columnDefs = [
      checkboxColumn,
      exampleColumnDefs.id,
      exampleColumnDefs.description
    ];

    // Set up the available context buttons.
    // If these are subject to user permissions, use the Protected component (src/components/shared/protected).
    this.contextBtns =
      <ComponentArray>
        <Btn svg={svgs.reconfigure} onClick={this.clickContextBtn('btn1')}>{props.t('walkthrough.pageWithGrid.grid.btn1')}</Btn>
        <Btn svg={svgs.trash} onClick={this.clickContextBtn('btn2')}>{props.t('walkthrough.pageWithGrid.grid.btn2')}</Btn>
      </ComponentArray>;
  }

  /**
   * Get the grid api options
   *
   * @param {Object} gridReadyEvent An object containing access to the grid APIs
  */
  onGridReady = gridReadyEvent => {
    this.gridApi = gridReadyEvent.api;
    // Call the onReady props if it exists
    if (isFunc(this.props.onGridReady)) {
      this.props.onGridReady(gridReadyEvent);
    }
  };

  clickContextBtn = (input) => () => {
    //Just for demo purposes. Don't console log in a real grid.
    console.log('Context button clicked', input);
    console.log('Hard selected rows', this.gridApi.getSelectedRows());
  };

  /**
   * Handles soft select props method.
   * Soft selection happens when the user clicks on the row.
   *
   * @param rowId The id of the currently soft selected item
   * @param rowData The rowData from the underlying grid. MAY BE OUT OF DATE.
   */
  onSoftSelectChange = (rowId, rowData) => {
    //Note: only the Id is reliable, rowData may be out of date
    const { onSoftSelectChange } = this.props;
    if (rowId) {
      console.log('Soft selected', rowId); //Just for demo purposes. Don't console log a real grid.
      this.setState({ softSelectedId: rowId });
    }
    if (isFunc(onSoftSelectChange)) {
      onSoftSelectChange(rowId, rowData);
    }
  }

  /**
   * Handles context filter changes and calls any hard select props method.
   * Hard selection happens when the user checks the box for the row.
   *
   * @param {Array} selectedObjs A list of currently selected objects in the grid
   */
  onHardSelectChange = (selectedObjs) => {
    const { onContextMenuChange, onHardSelectChange } = this.props;
    // Show the context buttons when there are rows checked.
    if (isFunc(onContextMenuChange)) {
      onContextMenuChange(selectedObjs.length > 0 ? this.contextBtns : null);
    }
    if (isFunc(onHardSelectChange)) {
      onHardSelectChange(selectedObjs);
    }
  }

  getSoftSelectId = ({ id } = '') => id;

  render() {
    const gridProps = {
      /* Grid Properties */
      ...defaultExampleGridProps,
      columnDefs: translateColumnDefs(this.props.t, this.columnDefs),
      sizeColumnsToFit: true,
      getSoftSelectId: this.getSoftSelectId,
      softSelectId: (this.state.softSelectedDevice || {}).id,
      ...this.props, // Allow default property overrides
      deltaRowDataMode: true,
      enableSorting: true,
      unSortIcon: true,
      getRowNodeId: ({ id }) => id,
      context: {
        t: this.props.t
      },
      /* Grid Events */
      onRowClicked: ({ node }) => node.setSelected(!node.isSelected()),
      onGridReady: this.onGridReady,
      onSoftSelectChange: this.onSoftSelectChange,
      onHardSelectChange: this.onHardSelectChange
    };

    return (
      <PcsGrid key="example-grid-key" {...gridProps} />
    );
  }
}

exampleGridConfig.js


import Config from 'app.config';
import { SoftSelectLinkRenderer } from 'components/shared/cellRenderers';

/** A collection of column definitions for the example grid */
export const exampleColumnDefs = {
  id: {
    headerName: 'walkthrough.pageWithGrid.grid.name',
    field: 'id',
    sort: 'asc',
    cellRendererFramework: SoftSelectLinkRenderer
  },
  description: {
    headerName: 'walkthrough.pageWithGrid.grid.description',
    field: 'descr'
  }
};

/** Given an example object, extract and return the device Id */
export const getSoftSelectId = ({ id }) => id;

/** Shared example grid AgGrid properties */
export const defaultExampleGridProps = {
  enableColResize: true,
  multiSelect: true,
  pagination: false,
  paginationPageSize: Config.paginationPageSize,
  rowSelection: 'multiple'
};

Copy the src/walkthrough/components/pages/pageWithGrid/exampleGrid folder to the src/components/pages/example folder.

Add the grid to the page

Modify the src/components/pages/example/basicPage.container.js as follows to import the service definitions:

import { connect } from 'react-redux';
import { translate } from 'react-i18next';

import {
  epics as exampleEpics,
  getExamples,
  getExamplesError,
  getExamplesLastUpdated,
  getExamplesPendingStatus
} from 'store/reducers/exampleReducer';
import { BasicPage } from './basicPage';

// Pass the data
const mapStateToProps = state => ({
  data: getExamples(state),
  error: getExamplesError(state),
  isPending: getExamplesPendingStatus(state),
  lastUpdated: getExamplesLastUpdated(state)
});

// Wrap the dispatch method
const mapDispatchToProps = dispatch => ({
  fetchData: () => dispatch(exampleEpics.actions.fetchExamples())
});

export const BasicPageContainer = translate()(connect(mapStateToProps, mapDispatchToProps)(BasicPage));

Modify the src/components/pages/example/basicPage.js as follows to add the grid:

// Copyright (c) Microsoft. All rights reserved.

import React, { Component } from 'react';

import {
  AjaxError,
  ContextMenu,
  PageContent,
  RefreshBar
} from 'components/shared';
import { ExampleGrid } from './exampleGrid';

import './basicPage.css';

export class BasicPage extends Component {
  constructor(props) {
    super(props);
    this.state = { contextBtns: null };
  }

  componentDidMount() {
    const { isPending, lastUpdated, fetchData } = this.props;
    if (!lastUpdated && !isPending) fetchData();
  }

  onGridReady = gridReadyEvent => this.gridApi = gridReadyEvent.api;

  onContextMenuChange = contextBtns => this.setState({ contextBtns });

  render() {
    const { t, data, error, isPending, lastUpdated, fetchData } = this.props;
    const gridProps = {
      onGridReady: this.onGridReady,
      rowData: isPending ? undefined : data || [],
      onContextMenuChange: this.onContextMenuChange,
      t: this.props.t
    };

    return [
      <ContextMenu key="context-menu">
        {this.state.contextBtns}
      </ContextMenu>,
      <PageContent className="basic-page-container" key="page-content">
        <RefreshBar refresh={fetchData} time={lastUpdated} isPending={isPending} t={t} />
        {!!error && <AjaxError t={t} error={error} />}
        {!error && <ExampleGrid {...gridProps} />}
      </PageContent>
    ];
  }
}

Modify the src/components/pages/example/basicPage.test.js as follows to update the tests:

// Copyright (c) Microsoft. All rights reserved.

import React from 'react';
import { shallow } from 'enzyme';
import 'polyfills';

import { BasicPage } from './basicPage';

describe('BasicPage Component', () => {
  it('Renders without crashing', () => {

    const fakeProps = {
      data: undefined,
      error: undefined,
      isPending: false,
      lastUpdated: undefined,
      fetchData: () => { },
      t: () => { },
    };

    const wrapper = shallow(
      <BasicPage {...fakeProps} />
    );
  });
});

Test the grid

If the web UI is not already running locally, run the following command in the root of your local copy of the repository:

npm start

The previous command runs the UI locally at https://localhost:3000/dashboard. Navigate to the Example page to see the grid display data from the service.

Select rows

There are two options for enabling a user to select rows in the grid:

Hard-select rows

If a user needs to act on multiple rows at the same time, use checkboxes on rows:

  1. Enable hard-selection of rows by adding a checkboxColumn to the columnDefs provided to the grid. checkboxColumn is defined in /src/components/shared/pcsGrid/pcsGrid.js:

    this.columnDefs = [
      checkboxColumn,
      exampleColumnDefs.id,
      exampleColumnDefs.description
    ];
    
  2. To access the selected items, you get a reference to the internal grid API:

    onGridReady = gridReadyEvent => {
      this.gridApi = gridReadyEvent.api;
      // Call the onReady props if it exists
      if (isFunc(this.props.onGridReady)) {
        this.props.onGridReady(gridReadyEvent);
      }
    };
    
  3. Provide context buttons to the page when a row in the grid is hard-selected:

    this.contextBtns = [
      <Btn key="context-btn-1" svg={svgs.reconfigure} onClick={this.doSomething()}>Button 1</Btn>,
      <Btn key="context-btn-2" svg={svgs.trash} onClick={this.doSomethingElse()}>Button 2</Btn>
    ];
    
    onHardSelectChange = (selectedObjs) => {
      const { onContextMenuChange, onHardSelectChange } = this.props;
      // Show the context buttons when there are rows checked.
      if (isFunc(onContextMenuChange)) {
        onContextMenuChange(selectedObjs.length > 0 ? this.contextBtns : null);
      }
      //...
    }
    
  4. When a context button is clicked, get the hard-selected items to do your work on:

    doSomething = () => {
      //Just for demo purposes. Don't console log in a real grid.
      console.log('Hard selected rows', this.gridApi.getSelectedRows());
    };
    

Soft-select rows

If the user only needs to act on a single row, configure a soft-select link for one or more columns in the columnDefs.

  1. In exampleGridConfig.js, add SoftSelectLinkRenderer as the cellRendererFramework for a columnDef.

    export const exampleColumnDefs = {
      id: {
        headerName: 'examples.grid.name',
        field: 'id',
        sort: 'asc',
        cellRendererFramework: SoftSelectLinkRenderer
      }
    };
    
  2. When a soft-select link is clicked, it triggers the onSoftSelectChange event. Perform whatever action is desired for that row, such as opening a details flyout. This example simply writes to the console:

    onSoftSelectChange = (rowId, rowData) => {
      //Note: only the Id is reliable, rowData may be out of date
      const { onSoftSelectChange } = this.props;
      if (rowId) {
        //Just for demo purposes. Don't console log a real grid.
        console.log('Soft selected', rowId);
        this.setState({ softSelectedId: rowId });
      }
      if (isFunc(onSoftSelectChange)) {
        onSoftSelectChange(rowId, rowData);
      }
    }
    

Next steps

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

Now you have defined a grid, the next step is to Add a custom flyout to the Remote Monitoring solution accelerator web UI that displays on the example page.

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