sencha-logoWith the successful roll-out of our workflow system, management has come up with another great idea: an Executive Dashboard. The main idea is that when anyone with the “Admin” role logs in, they should be greeted with a “dashboard” of sorts that will allow them to see a variety of details from across the application, such as snapshots of the charts we previously built, outstanding workflow actions, and the most recently added inventory records.

Of course, we’ve already built all this stuff, so we confidently respond that this is no problem, and we’ll have it done in a jiffy. So let’s get jiffy-ing!

NOTE: Code for this installment can be found on GitHub.

Strategy

One of the best things about the way we’ve built our ExtJS 4 app is that we can now really start to benefit from reuse of code. Instead of having to re-create all the components (charts, grids, etc), we can simply instantiate them in new contexts and move on with our lives.

For this Executive Dashboard, we’ll reuse both of the charts that we created earlier, as we all reusing the inventory grid twice. Since we’ve already created stores and models for all of these, there’s ZERO that we have to create in this regard. Hooray!

To organize things, we’ll utilize the Ext.layout.container.Border layout as well as the Ext.layout.container.Anchor layout to create a nice “quadrant” effect. Ultimately, we want something like this:

Dashboard

The Dashboard

Our dashboard view (view/executive/Dashboard.js) is very straightforward. Since the components we want to inject into our dashboard already exist, we simply need to instantiate them via xtype…just like we’ve been doing all along:

Ext.define('CarTracker.view.executive.Dashboard', {
    extend: 'Ext.panel.Panel',
    alias: 'widget.executive.dashboard',
    layout: 'border',
    initComponent: function() {
        var me = this;
        Ext.applyIf(me, {
            items: [
                // column 1
                {
                    xtype: 'container',
                    region: 'west',
                    width: '50%',
                    layout: 'anchor',
                    split: true,
                    items: [
                        {
                            xtype: 'panel',
                            title: 'Monthly Sales',
                            iconCls: 'icon_barchart',
                            anchor: '100% 50%',
                            layout: 'fit',
                            items: [
                                {
                                    xtype: 'report.month.chart',
                                    store: {
                                        type: 'report.month'
                                    }
                                }
                            ]
                        },
                        {
                            xtype: 'car.list',
                            dockedItems: false,
                            title: 'Latest Additions',
                            anchor: '100% 50%',
                            store: {
                                type: 'car',
                                pageSize: 6,
                                sorters: [
                                    {
                                        property: 'CreatedDate',
                                        direction: 'DESC'
                                    }
                                ]
                            }

                        }
                    ]
                },
                // column 2
                {
                    xtype: 'container',
                    region: 'center',
                    width: '50%',
                    layout: 'anchor',
                    items: [
                        {
                            xtype: 'car.list',
                            dockedItems: false,
                            anchor: '100% 50%',
                            title: 'Audits Awaiting Approval',
                            iconCls: 'icon_workflow',
                            viewConfig: {
                                deferEmptyText: false,
                                emptyText: 'No audits awaiting approval!',
                                markDirty: false
                            },
                            store: {
                                type: 'car',
                                remoteFilter: true,
                                pageSize: 6,
                                sorters: [
                                    {
                                        property: 'CreatedDate',
                                        direction: 'DESC'
                                    }
                                ],
                                listeners: {
                                    beforeload: function( store, operation, eOpts ) {
                                        store.getProxy().extraParams = {
                                            filter: Ext.encode([
                                                {
                                                    property: 'Status',
                                                    value: [3]
                                                }
                                            ]) 
                                        };
                                    }
                                }
                            }
                        },
                        {
                            xtype: 'panel',
                            title: 'Sales by Model',
                            iconCls: 'icon_piechart',
                            anchor: '100% 50%',
                            layout: 'fit',
                            items: [
                                {
                                    xtype: 'report.make.chart',
                                    title: 'Sales by Make',
                                    store: {
                                        type: 'report.make'
                                    }
                                }
                            ]
                        }
                    ]
                }
            ],
        });
        me.callParent( arguments );
    }
});

This looks pretty much like any of our other views. However, I will point out a few things highlighted in bold.

First, for each of our border layout’s regions, we create containers that have an anchor layout. The Ext.layout.container.Anchor allows us to “anchor” child items to positions relative to the parent. In this case, we define that each child item should occupy 100% width and 50% height of the containing element. And if we ever resize, our child items will resize along with the parent.

Next, you’ll notice that for both of our charts, we override the store that they should use at instantiation. If you remember, our actual chart components have inline, non-proxy-ed stores that are loaded with data from their sibling grids. Obviously, since the grids are not available on the dashboard, we need to get that data elsewhere. While we could refactor our charts, we can also simply override the settings as needed. In this case, that’s what we’ll do.

Finally, and related, you’ll notice that we’re doing something similar on our inventory grids. Again, we could refactor our original approach entirely, or even create new custom components; but passing new configs is part of the beauty of the ExtJS 4 component architecture, so taking advantage of it is what will help us fully realize the power of component reuse.

Of course, the best part of all of this is that because we are reusing these components, all of the functionality that we’ve bound to them (e.g., the inventory grid’s contextmenu events, etc.) are all still active. So our Admin role users can effectively manage their portion of the application from the dashboard without having to navigate to the “official” section of the app. Do I sense a bonus in our future?

A New Controller

While we don’t technically need it, let’s go ahead and create a new Dashboard.js controller. Executive Dashboards have a funny way of growing in complexity very quickly, so by creating a separate controller we are merely future-proofing ourselves against the inevitable.

This new controller is going to be very thin–in fact, the only real work that it will do is to define a beforerender listener for our dashboard component, and call a method which will handle loading any data that we need for our dashboard view’s components:

/**
 * Controller for Executive Dashboard functionality
 */
Ext.define('CarTracker.controller.Dashboard', {
    extend: 'CarTracker.controller.Base',
    stores: [
        'report.Makes',
        'report.Months'
    ],
    views: [
        'executive.Dashboard',
        'report.make.Chart',
        'report.month.Chart',
        'car.List'
    ],
    init: function() {
        this.listen({
            controller: {},
            component: {
                '[xtype=executive.dashboard]': {
                    beforerender: this.loadDashboards,
                }
            },
            global: {},
            store: {},
            proxy: {} 
        });
    },
    /**
     * Handles initial loading of the executive dashboard
     * @param {Ext.panel.Panel} panel
     * @param {Object} eOpts
     */
    loadDashboards: function( panel, eOpts ) {
        var me = this,
            makereport = panel.down( '[xtype=report.make.chart]' ),
            monthreport= panel.down( '[xtype=report.month.chart]' );
        // call report stores manually
        makereport.getStore().load({
            params: {
                filter: Ext.encode([
                    {
                        property: 'SalesByMake',
                        value: true
                    },
                    {
                        property: 'IsSold',
                        value: true
                    }
                ])
            }
        });
        monthreport.getStore().load({
            params: {
                filter: Ext.encode([
                    {
                        property: 'SalesByMonth',
                        value: true
                    },
                    {
                        property: 'IsSold',
                        value: true
                    }
                ])
            }
        });
    }
});

NOTE: An important thing to be aware of is that we’ve already bound a beforerender event in our Cars.js controller to the inventory grid component. Because ExtJS controller listeners fire across the entire application, the loadRecords() method defined in Cars.js will be run when we render the inventory grid components on the dashboard.

While this is desirable behavior in our scenario (e.g., we want data to be loaded on render), it can also create conflicts if we are not careful. This is one very good reason to be as specific in your component selectors as possible.

Nothing to it. With that, everything is working. The last bit is to add a new menu item (restricted to only Admin roles), and update our App.js to handle the history event:

view/layout/Menu.js:

{
    text: 'Executive Dashboard',
    itemId: 'dashboard',
    iconCls: 'icon_dashboard',
    hidden: !CarTracker.LoggedInUser.inRole( 1 )
}

controller/App.js:

dispatch: function( token ) {
    ...
    switch( token ) {
        ...
        case 'dashboard':
            allowedRole = 1;
            config = {
                xtype: 'executive.dashboard'
            };
            break;
        ...
    }
    ...
}

Wrapping Up

That was cake, right? We had to do almost ZERO extra development in order to pull this off, and because of the inherent shininess of dashboards  we’ll probably get more kudos from the check-writers than from anything that took us hours of sweat and tears to implement.