Tutorial overview

This is "UCW Report" tutorial of a UCW portal add-on. Throughout the tutorial, you will build few reports, which typically consist of a graph and table. UCW provides graph interface using Rickshaw graphs, and you will learn how to use it. You will also need knowledge from Hello World Tutorial and UCW Job Scheduler. If you have not completed them yet, please see the tutorial pages. See source code

Requirements 

Step 01: Download tutorial sources

To download tutorial sources and test their functionality, do as follows:

  1. Make sure you have downloaded all the requirements.
  2. Open a terminal/console and navigate to your working directory.
  3. Download the sources using the following command:

    git clone https://github.com/unitycloudware/ucw-portal-report.git ucw-portal-report
  4. Move into a newly created folder.

    cd ucw-portal-report
  5. Checkout to the starting point of the tutorial.

    git checkout start

    Note

    In the tutorial, you will be working in "start" branch. However, at any moment of the tutorial, you can checkout into "master" branch to see correct solution.

  6. Test downloaded files by building and running the sources. In order to do so, execute following commands:

    Unix / Linux based OS
    ./build.sh
    ./run-portal.sh

    or

    Windows
    build.bat
    run-portal.bat
  7. The application should be up and running on http://localhost:9572 (TODO - add a link to setting tomcat port section). After clicking the link, you should see a login page. 

  8. Log into the application using default credentials (User: admin, Password: admin) and admin page should be displayed. (TODO - add a link to credential settings)


You just setted a necessary environment for developing your first report.

As you can see, there is a lot of stuff that has been already created. All of it has been created using UCW Template Module. If you have not familiarised with it yet, please see its documentation for more information.

Step 02: Sample Report

If you have already worked with the UCW Platform, you could have noticed that there is a Report section under Top Navigation Menu (TODO - add a link?). In this step, you will create "Sample Report" which will be a copy of the existing one. Complete the following steps to do so:

  1. Create sample data file sample-chart-data.json with the following content:

    [
      {
        "color": "#3b7fc4",
        "name": "London, UK",
        "key": "chart1",
        "data": [ { "x": 0, "y": 40 }, { "x": 1, "y": 49 }, { "x": 2, "y": 38 }, { "x": 3, "y": 30 }, { "x": 4, "y": 32 } ]
      },
      {
        "color": "#8eb021",
        "name": "San Francisco, CA, USA",
        "key": "chart2",
        "data": [ { "x": 0, "y": 19 }, { "x": 1, "y": 22 }, { "x": 2, "y": 29 }, { "x": 3, "y": 20 }, { "x": 4, "y": 14 } ]
      },
      {
        "color": "#654982",
        "name": "Vancouver, BC, Canada",
        "key": "chart3",
        "data": [ { "x": 0, "y": 8 }, { "x": 1, "y": 12 }, { "x": 2, "y": 15 }, { "x": 3, "y": 11 }, { "x": 4, "y": 10 } ]
      }
    ]
  2. Create sample-report.vm using velocity template.

    $portalResourceManager.requireResourcesForContext("nsys.portal.chart")
    
    <div class="chart-center">
        #foreach ($chart in $charts)
            <div class="aui-group">
                <div class="aui-item">
                    $!{chart}
                </div>
            </div>
        #end
    </div>
  3. Create SampleReport class and extend AbstractReportController (TODO - add link)
  4. Implement configure method using the following snippet:

    setReportName("UCW Report - Sample");
    setReportImageUrl("${portalResourcesUrl}/resources/images/ucw_logo.png");
    setReportTemplate("/templates/report/sample-report.vm");
    setChartTitle("UCW Report - Sample Chart # 1");
    setChartType(ChartConfig.ChartType.BAR);
    setChartDataUrl("/ucw-report/sample/chart-data");
    
    ChartConfig chart2 = getDefaultChartConfig().clone();
    chart2.setChartTitle("UCW Report - Sample Chart # 2");
    chart2.setChartType(ChartConfig.ChartType.LINE);
    addChartConfig(chart2);
    
    ChartConfig chart3 = getDefaultChartConfig().clone();
    chart3.setChartTitle("UCW Report - Sample Chart # 3");
    chart3.setChartType(ChartConfig.ChartType.AREA);
    addChartConfig(chart3);

    Note

    Note that only setReportTemplate and setChartDataUrl needs to be called (Other methods are called only to make the report look better). Also, note that you do not have to always work with multiple series and so only one chartConfig would be necessary for the configure method.

  5. Implement showReport function

    @RequestMapping(method = RequestMethod.GET)
    @Override
    public ModelAndView showReport(final HttpServletRequest request, final HttpServletResponse response) {
        Map<String, Object> context = new HashMap<String, Object>();
        return render(context, request, response);
    }
  6. Implement endpoint for Rickshaw graph which returns the sample-chart-data.json data. It URL is same as you have filled in setChartDataUrl.

    @RequestMapping(value = "/chart-data", method = RequestMethod.GET)
    @ResponseBody
    public String getChartData(final HttpServletRequest request, final HttpServletResponse response) {
        return IOUtils.toString(getClass().getResourceAsStream("/data/sample-chart-data.json"));
    }
  7. Build and run the application and navigate to http://localhost:9572/ucw-report/sample

  8. The result should be same as on page http://localhost:9572/report/sample where you can find default Sample Report


That's it. You have created your first report, although it is not directly accessible anywhere from the application. Continue to the next step to add links to your report. 

Step 03: Add links to report

To add a link to the report, you will use navigation section and navigation item. Try to add navigation item into "Report" section (section id is 'system.top.navigation.bar/reports'). In this step new thing, you will learn how to add a link to the sidebar menu. Use navigation section and navigation item as follows:

  1. Open nsys-plugin.xml
  2. Add the following snippet

    <navigation-section key="ucw-portal-tutorial_reports-available" name="UCW Demo Reports Navigation"
                        location="system.report/ucw.demo" weight="0">
        <label>UCW Demo Reports</label>
    </navigation-section>
    
    <navigation-item key="ucw-report_reports-available_sample" name="UCW Sample Report"
                     section="system.report/ucw.demo" weight="0">
        <label>UCW Report - Sample</label>
        <link>/ucw-report/sample</link>
    </navigation-item>
  3. Build and run the application

See that there are two new links. One in the top navigation menu and one under "UCW DEMO REPORTS" section in the left sidebar menu. That's it. You have learned how to work with UCW Reports. Next steps are just extensions of the same thing. Continue reading to learn how to create more complex reports with random data or data from a device. 

Step 04: Temperature Report

This step builds on knowledge from Step 02 and Step 03, and as a result of this step, you will obtain a report with randomly generated temperature data.

  1. Copy content of sample-report.vm into new file temperature-report.vm
  2. Copy content of SampleReport class into new class TemperatureReport
  3. In the TemperatureReport class change all request mappings to avoid collisions with mappings from SampleReport
  4. Change REPORT_TEMPLATE, so that is points to temperature-report.vm. You can also change title, name and other variables so that they fit more to our use case.
  5. Remove additional ChartConfigs
  6. Update getChartData function, so that generates random data. You can use the following snippet:

    @RequestMapping(value = "/chart-data", method = RequestMethod.GET)
    @ResponseBody
    public List<ChartSeries> getChartData(
            final HttpServletRequest request,
            final HttpServletResponse response) {
    
        ChartData data = new ChartData();
    
        data.addSeries(
                getTemperatureData(
                        "temperatureData",
                        "DHT22 Sensor # 1",
                        ChartConfig.ChartColor.BLUE));
    
        return data.getSeries();
    }
    
    protected ChartSeries getTemperatureData(
            final String key,
            final String name,
            final String color) {
    
        ChartSeries temperatureData = new ChartSeries();
        temperatureData.setKey(key);
        temperatureData.setName(name);
        temperatureData.setColor(color);
    
        Date date = TimeUtils.getStartOfDay(TimeUtils.getNow());
    
        for (int i = 0; i < 24; i++) {
            double value = RandomRange.getRandomDouble(1, 100);
            long timestamp = date.getTime() + TimeUtils.getTimezoneUTCAndDSTOffset(date);
            temperatureData.addPoint(ChartPoint.<Long, Double>create(timestamp / 1000, value));
            date = TimeUtils.addHours(date, 1);
        }
    
        return temperatureData;
    }
  7. Additionally, add links to the temperature report

  8. Build and run the application. As a result, you should see a page similar to the following one


Step 05: Temperature Report with data from a data stream

In this step, you will modify the previous report so that renders data from a data stream. Add SensorDataGeneratorJob, TestDataUtils and make use of them as explained in UCW Job Scheduler so that you have a data to show. Once you added the components you can follow next steps:

  1. Make a copy of TemplateReport 
  2. Change all request mappings to avoid collisions with mappings from original file
  3. Change the value of setChartAxisXTimeUnit to "10 second" since the job generates data each ten seconds
  4. Update getChartData so that it reads from DataStream using TestDataUtils

    @RequestMapping(value = "/chart-data", method = RequestMethod.GET)
    @ResponseBody
    public List<ChartSeries> getChartData(
            final HttpServletRequest request,
            final HttpServletResponse response) {
    
        ChartData data = new ChartData();
    
        DataStream dataStream = getTestDataUtils().getDataStream();
    
        List<String> colors = Arrays.asList(
                ChartConfig.ChartColor.BLUE, ChartConfig.ChartColor.GREEN, ChartConfig.ChartColor.VIOLET);
    
        int counter = 0;
    
        for (Device device : getTestDataUtils().getDevices()) {
            data.addSeries(getTemperatureData(dataStream, device, colors.get(counter)));
            counter++;
        }
    
        return data.getSeries();
    }
    
    protected ChartSeries getTemperatureData(
            final DataStream dataStream,
            final Device device,
            final String color) {
    
        ChartSeries temperatureData = new ChartSeries();
        temperatureData.setKey(String.format("%s_id", device.getName()));
        temperatureData.setName(device.getName());
        temperatureData.setColor(color);
    
        List<DataStreamItem> items = getDataManager().loadStream(dataStream, device, 0, 18);
    
        if (items == null || items.isEmpty()) {
            return temperatureData;
        }
    
        for (DataStreamItem item : items) {
            if (item.getType() != DataStreamType.DATA_MESSAGE) {
                continue;
            }
    
            DataMessage dataMessage = (DataMessage) item.getData();
    
            DataItem di = JsonUtils.fromJson(dataMessage.getData(), DataItem.class);
    
            temperatureData.addPoint(ChartPoint.<Long, Double>create(di.getTimestamp() / 1000, di.getTemperature()));
        }
    
        return temperatureData;
    }
  5. Add necessary class attributes and their getter functions:

    private DeviceManager deviceManager;
    private DataManager dataManager;
    private TestDataUtils testDataUtils;
    
    public DeviceManager getDeviceManager() {
        if (deviceManager == null) {
            deviceManager = ComponentProvider.getInstance().getComponent(DeviceManager.class);
        }
    
        return deviceManager;
    }
    
    public DataManager getDataManager() {
        if (dataManager == null) {
            dataManager = ComponentProvider.getInstance().getComponent(DataManager.class);
        }
    
        return dataManager;
    }
    
    public TestDataUtils getTestDataUtils() {
        if (testDataUtils == null) {
            testDataUtils = ComponentProvider.getInstance().getComponent(TestDataUtils.class);
        }
    
        return testDataUtils;
    }
  6. Create DataItem model

    @JsonIgnoreProperties(ignoreUnknown = true)
    public class DataItem {
        private double temperature;
        private int humidity;
        private long timestamp;
    
    	Getters/Setters
    
    	public static DataItem create(final double temperature, final int humidity, final long timestamp) {
        	DataItem item = new DataItem();
    	    item.setTemperature(temperature);
    	    item.setHumidity(humidity);
    	    item.setTimestamp(timestamp);
        	return item;
    	}
    }
  7. Additionally, add links to the temperature report

  8. Build and run the application. As a result, you should see a page similar to the following one

Step 06: Humidity Report

If you have a look at the TemperatureReport and reload the page multiple times you will notice that the data changes every time. Let's create HumidityReport which will always the same data (Although the data will be still randomly generated at this moment) and also show the data in a table. Follow the steps:

  1. Make a copy of sample-report.vm and add the following snippet to the file

    <table class="aui aui-table-interactive aui-table-sortable">
       <thead>
       <tr>
          <th id="data-humidity">Humidity</th>
          <th id="data-timestamp">Timestamp</th>
       </tr>
       </thead>
       <tbody>
               #foreach ($d in $data)
          <tr>
             <td headers="data-humidity"><span class="aui-lozenge aui-lozenge-current">$!{d.value}%</span></td>
             <td headers="data-timestamp">$date.format("yyyy-MM-dd HH:mm:ss", $!{d.timestamp})</td>
          </tr>
               #end
       </tbody>
    </table>
  2. Make a copy of TemperatureReport class

  3. Change all request mappings to avoid collisions with mappings from TemperatureReport class
  4. Change REPORT_TEMPLATE variable so that it points to newly created velocity template
  5. Create HumidityData model

    @JsonIgnoreProperties(ignoreUnknown = true)
    public class HumidityData {
        private int value;
        private long timestamp;
    
        public static HumidityData create(final int value, final long timestamp) {
            HumidityData humidityData = new HumidityData();
            humidityData.setValue(value);
            humidityData.setTimestamp(timestamp);
            return humidityData;
        }
    
    
    
    
    	Getters/Setters
    }
  6. Update showReport function

    @RequestMapping(method = RequestMethod.GET)
    @Override
    public ModelAndView showReport(
            final HttpServletRequest request,
            final HttpServletResponse response) {
    
        if (cachedData == null) {
            cachedData = getData();
        }
    
        Map<String, Object> context = new HashMap<String, Object>();
        context.put("data", cachedData);
    
        return render(context, request, response);
    }
  7. Update getChartData with the following snippet:

    @RequestMapping(value = "/chart-data", method = RequestMethod.GET)
    @ResponseBody
    public List<ChartSeries> getChartData(
            final HttpServletRequest request,
            final HttpServletResponse response) {
    
        ChartData data = new ChartData();
    
        if (cachedData == null) {
            cachedData = getData();
        }
    
        data.addSeries(
                getHumidityData(
                        "humidityData",
                        "DHT22 Sensor # 1",
                        ChartConfig.ChartColor.BLUE,
                        cachedData));
    
        return data.getSeries();
    }
    
    protected ChartSeries getHumidityData(
            final String key,
            final String name,
            final String color,
            final List<HumidityData> data) {
    
        ChartSeries humidityData = new ChartSeries();
        humidityData.setKey(key);
        humidityData.setName(name);
        humidityData.setColor(color);
    
        for (HumidityData hd : data) {
            humidityData.addPoint(ChartPoint.<Long, Integer>create(hd.getTimestamp() / 1000, hd.getValue()));
        }
    
        return humidityData;
    }
    
    protected List<HumidityData> getData() {
        List<HumidityData> humidityData = new ArrayList<HumidityData>();
    
        Date date = TimeUtils.getStartOfDay(TimeUtils.getNow());
    
        for (int i = 0; i < 24; i++) {
            int value = RandomRange.getRandomInt(1, 100);
            long timestamp = date.getTime() + TimeUtils.getTimezoneUTCAndDSTOffset(date);
            humidityData.add(HumidityData.create(value, timestamp));
            date = TimeUtils.addHours(date, 1);
        }
    
        return humidityData;
    }
  8. Additionally, add links to the temperature report

  9. Build and run the application. As a result, you should see a page similar to the following one

Step 07: Interactive HumidityReport

In the last step, of this tutorial, you will implement a report which extends the HumidityReport in a way that it also loads data from a DataStream and also adds some interactive element. Using the element user will be able to select device for which data in a table is shown. Let's put all the pieces you have learned to create the report

  1. Make a copy of temperature-report.vm file and paste the following snippet into it

    <form id="report-humidity2-device" action="$!{request.contextPath}/ucw-report/humidity2/device" method="POST" class="aui">
     <label for="deviceId">Device</label>
     <select class="select field" id="deviceId" name="deviceId" title="Device Select">
     #foreach ($d in $devices)
                <option value="$!{d.deviceId}" #if ($!{d.deviceId} == $!{device.deviceId}) selected #end>$!{d.name}</option>
     #end
     </select>
     &nbsp;
     <input class="aui-button aui-button-primary submit" type="submit" value="Show" id="btnSubmit"><br/>
     <br/>
     <table class="aui aui-table-interactive aui-table-sortable">
     <thead>
     <tr>
     <th id="data-humidity">Humidity</th>
     <th id="data-timestamp">Timestamp</th>
     </tr>
     </thead>
     <tbody>
     #foreach ($d in $data)
                <tr>
     <td headers="data-humidity"><span class="aui-lozenge aui-lozenge-current">$!{d.humidity}%</span></td>
     <td headers="data-timestamp">$date.format("yyyy-MM-dd HH:mm:ss", $!{d.timestamp})</td>
     </tr>
     #end
     </tbody>
     </table>
    </form>
  2. Make a copy of extended TemperatureReport class (the class created in Step 05). 

  3. Change all request mappings to avoid collisions with mappings from extended TemperatureReport class
  4. Change REPORT_TEMPLATE variable so that it points to newly created velocity template
  5. Change showReport function with the following code

    @RequestMapping(method = RequestMethod.GET)
    @Override
    public ModelAndView showReport(
            final HttpServletRequest request,
     final HttpServletResponse response) {
    
        List<Device> devices = getTestDataUtils().getDevices();
    
     return showDeviceReport(devices.get(0).getDeviceId(), request, response);
    }
    
    @RequestMapping(value = "/device", method = RequestMethod.POST)
    public ModelAndView showDeviceReport(
            @RequestParam("deviceId") final String deviceId,
     final HttpServletRequest request,
     final HttpServletResponse response) {
    
        Device device = getDeviceManager().getDevice(deviceId);
    
     Map<String, Object> context = new HashMap<String, Object>();
     context.put("device", device);
     context.put("devices", getTestDataUtils().getDevices());
     context.put("data", getData(getTestDataUtils().getDataStream(), device));
    
     return render(context, request, response);
    }
  6. Update getChartData function so that returns data obtained from a data stream

    @RequestMapping(value = "/chart-data", method = RequestMethod.GET)
    @ResponseBody
    public List<ChartSeries> getChartData(
            final HttpServletRequest request,
     final HttpServletResponse response) {
    
        ChartData data = new ChartData();
    
     DataStream dataStream = getTestDataUtils().getDataStream();
    
     if (dataStream == null) {
            return data.getSeries();
     }
    
        List<String> colors = Arrays.asList(
                ChartConfig.ChartColor.BLUE, ChartConfig.ChartColor.GREEN, ChartConfig.ChartColor.VIOLET);
    
     int counter = 0;
    
     for (Device device : getTestDataUtils().getDevices()) {
            data.addSeries(getHumidityData(dataStream, device, colors.get(counter)));
     counter++;
     }
    
        return data.getSeries();
    }
    
    protected ChartSeries getHumidityData(
            final DataStream dataStream,
     final Device device,
     final String color) {
    
        ChartSeries humidityData = new ChartSeries();
     humidityData.setKey(String.format("%s_id", device.getName()));
     humidityData.setName(device.getName());
     humidityData.setColor(color);
    
     List<DataItem> items = getData(dataStream, device);
    
     if (items == null || items.isEmpty()) {
            return humidityData;
     }
    
        for (DataItem item : items) {
            humidityData.addPoint(ChartPoint.<Long, Integer>create(item.getTimestamp() / 1000, item.getHumidity()));
     }
    
        return humidityData;
    }
    
    protected List<DataItem> getData(
            final DataStream dataStream,
     final Device device) {
    
        List<DataItem> data = new ArrayList<DataItem>();
    
     // Load 18 last records
     List<DataStreamItem> items = getDataManager().loadStream(dataStream, device, 0, 18);
    
     if (items == null || items.isEmpty()) {
            return data;
     }
    
        for (DataStreamItem item : items) {
            // Process only data for data stream with type of DATA_MESSAGE
     if (item.getType() != DataStreamType.DATA_MESSAGE) {
                continue;
     }
    
            DataMessage dataMessage = (DataMessage) item.getData();
    
     // Transform JSON payload to DataItem object
     data.add(JsonUtils.fromJson(dataMessage.getData(), DataItem.class));
     }
    
        return data;
    }
  7. Additionally, add links to the temperature report

  8. Build and run the application. As a result, you should see a page similar to the following one. If you change the value in "Device" select box, the page rerenders and shows data for the selected device in a table below the graph.


  • No labels