Monday, November 13, 2017

An AngularJS Dashboard, Part 6: Admin Tile Actions and Confirmation Dialogs

NOTE: for best results, view the http: version of this page (else you won't get syntax highlighting).

This is Part 6 in a series on creating a dashboard in AngularJS. I'm blogging as I progressively create the dashboard, refining my Angular experience along the way.

Previously in Part 5, we added a user interface for configuring tiles, dashboard layout persistence, and tile menu actions. We also admitted a lot of the code was new and hadn't gone through a deep test and debug cycle.

Today, we're going to:

  • Polish the existing tile actions with confirmation dialogs.
  • Add a new tile action, Make Default Layout, for setting the default dashboard (available only to administrators).
  • Pass user information and privileges from the MVC back end to the Angular code.
  • Improve the appearance of the Table tile.
  • Release updated code with bug fixes.
Here' a view of what we'll be ending up with today:


Confirmation Dialogs

Last time, we added a number of tile menu actions. Some of these, like Remove Tile or Reset Dashboard, delete parts of your saved dashboard layout. We shouldn't be taking potentially destructive actions without being sure it's what the user wants, so we're now going to add confirmation dialogs for these actions.

The approach we'll be taking to confirmation dialogs is to use leverage Bootstrap, which has been part of our soluton all along. We'll be re-using the same confirmation dialog for each confirmation, so let's add that markup to our HTML template. The dialog has id confirm-reset-modal.
<!-- Confirmation dialog -->

<div class="modal fade" tabindex="-1" role="dialog" aria-labelledby="confirm-label" aria-hidden="true" id="confirm-reset-modal">
    <div class="modal-dialog modal-md">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
                <h4 class="modal-title" id="confirm-label">Confirmation Message</h4>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-success" id="modal-btn-yes">Yes</button>
                <button type="button" class="btn btn" id="modal-btn-no">No</button>
            </div>
        </div>
    </div>
</div>
HTML template with confirmation dialog markup

Previously, the tile menu actions simply invoked functons (removeTile, etc.) that called counterpart functions in the controller, taking immediate action. Now, we'll be calling confirmation functions first that will ask the user if they are sure they want to take the selected action. Only in the case of a Yes response will the action take place. Our first step, then, is to update the tile menu markup to call confirmation functions.
<!-- Populated tile (data loaded) -->
<div id="tile-{{tile.id}}" ng-if="tile.haveData"
        class="tile" ng-class="tile.classes" ng-style="{ 'background-color': $ctrl.tileColor(tile.id), 'color': $ctrl.tileTextColor(tile.id) }"
        style="overflow: hidden"
        draggable="true" ondragstart="tile_dragstart(event);"
        ondrop="tile_drop(event);" ondragover="tile_dragover(event);">
    <div class="dropdown" style="height: 100%">
        <div class="hovermenu">
            <i class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-hidden="true"></i>
            <ul class="dropdown-menu" style="margin-left:-150px !important">
                <li><a id="tile-config-{{tile.id}}" href="#" onclick="configureTile(this.id);"><i class="fa fa-gear" aria-hidden="true"></i>  Configure Tile</a></li>
                <li><a href="#" onclick="configureTile('0');"><i class="fa fa-plus-square-o" aria-hidden="true"></i>  Add Tile</a></li>
                <li><a id=tile-remove-{{tile.id}}" href="#" onclick="removeTileConfirm(this.id);"><i class="fa fa-trash-o" aria-hidden="true"></i>  Remove Tile</a></li>
                <li><a id=tile-reset-{{tile.id}}" href="#" onclick="resetDashboardConfirm();"><i class="fa fa-refresh" aria-hidden="true"></i>  Reset Dashboard</a></li>
                <li ng-if="$ctrl.user.IsAdmin"><a id=tile-reset-{{tile.id}}" href="#" onclick="saveDefaultDashboardConfirm();"><i class="fa fa-check-square-o" aria-hidden="true"></i>  Make Default Layout</a></li>
            </ul>
        </div>
Tile menu updated to call confirmation functions

We've switched to calling confirmation functions for Remove Tile, Reset Default Dashboard, and our new action Make Default Layout.

Remove Tile

The confirmation function for Remove Tile is shown below. Line 4 sets the confirmation message; lines 6-15 set handlers for the Yes and No buttons; and line 17 displays the confirmation dialog. If Yes, is selected, the original removeTile function is executed to remove the tile and the modal dialog is hidden. If No is selected, no action ensues and the modal dialog is hidden.
// removeTile : Remove a tile.

function removeTileConfirm(id) {
    $('#confirm-label').html('Are you sure you want to remove this tile?');

    $("#modal-btn-yes").on("click", function () {
        $("#modal-btn-yes").off();
        $("#modal-btn-no").off();
        removeTile(id);
        $("#confirm-reset-modal").modal('hide');
    });

    $("#modal-btn-no").on("click", function () {
        $("#confirm-reset-modal").modal('hide');
    });

    $("#confirm-reset-modal").modal('show');
}

function removeTile(id) {
    var scope = angular.element('#dashboard').scope();
    var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;

    scope.$evalAsync(function () {
        return ctrl.removeTile(id);
    });
}

Updated JavaScript code for Remove Tile

When the user selectes Remove Tile from the tile menu, they now see this confirmation dialog:

Remove Tile Confirmation Dialog

Clicking Yes proceeds to remove the tile, by calling the original removeTile function.

Reset Dashboard

Following the exact same pattern, here is the code for Reset Dashboard.
// resetDashboard : Restore default dashboard by deleting user's saved custom dashboard.

function resetDashboardConfirm() {
    $('#confirm-label').html('Are you sure you want to reset your dashboard to the default layout?');

    $("#modal-btn-yes").on("click", function () {
        $("#modal-btn-yes").off();
        $("#modal-btn-no").off();
        resetDashboard();
        $("#confirm-reset-modal").modal('hide');
    });

    $("#modal-btn-no").on("click", function () {
        $("#confirm-reset-modal").modal('hide');
    });

    $("#confirm-reset-modal").modal('show');
}

function resetDashboard() {
    var scope = angular.element('#dashboard').scope();
    var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;

    scope.$evalAsync(function () {
        return ctrl.resetDashboard();
    });
}
When the Reset Dashboard tile action is taken, the user sees this confirmation dialog.

Reset Dashboard Confirmation Dialog

Clicking Yes resets the dashboard, by calling the original resetDashboard function.

New Tile Action: Make Default Layout

We're adding a new tile action named Make Default Layout. Up till now, we've seen that we have a default layout defined in the database; and that we can persist a customized layout for a user. What's been lacking is a way to take a customized layout and make it the new default for all users. That's what this new action will do.


// saveDefaultDashboard : Save current layout as the default layout for all users.

function saveDefaultDashboardConfirm() {
    $('#confirm-label').html('Are you sure you want to make this layout the default for all users?');

    $("#modal-btn-yes").on("click", function () {
        $("#modal-btn-yes").off();
        $("#modal-btn-no").off();
        saveDefaultDashboard();
        $("#confirm-reset-modal").modal('hide');
    });

    $("#modal-btn-no").on("click", function () {
        $("#confirm-reset-modal").modal('hide');
    });

    $("#confirm-reset-modal").modal('show');
}

function saveDefaultDashboard() {
    console.log('saveDefaultDashboard');
    var scope = angular.element('#dashboard').scope();
    var ctrl = angular.element('#dashboard').scope().$$childHead.$ctrl;

    scope.$evalAsync(function () {
        return ctrl.saveDefaultDashboard();
    });
}


Make Default Layout Confirmation Dialog

Below is the new controller function saveDefaultDashboard. This code leverages existing controller code and Data Service code for saving a dashboard, but an isDefault flag has been added to DataService.saveDashboard. When a true is passed to DataService.saveDashboard, that means we are updating the default dashboard rather than the user's customized dashboard.

// saveDefaultDashboard : make user's dashboard the default dashboard for all users

self.saveDefaultDashboard = function () {
    self.wait(true);
    DataService.saveDashboard(self.tiles, true);
    toastr.options = {
        "positionClass": "toast-top-center",
        "timeOut": "1000",
    }
    toastr.info('Default Dashboard Layout Saved');
    self.wait(false);
};
Controller saveDefaultDashboard function

In DataService.saveDashboard (SQL Server version shown below), the only change is the addition of the isDefault flag, which is passed in the structure sent to the SaveDashboard MVC action.

// -------------------- saveDashboard : updates the master layout for tiles (returns tiles array). If isDefault=true, this becomes the new default dashboard layout. ------------------

self.saveDashboard = function (newTiles, isDefault) { 

    var Dashboard = {
        DashboardName: null,
        IsAdmin: false,
        Username: null,
        Tiles: [],
        Queries: null,
        IsDefault: isDefault
    };

    var tile = null;
    var Tile = null;

    // create tile object with properties

    for (var t = 0; t < newTiles.length; t++) {
        tile = newTiles[t];
        Tile = {
            Sequence: t+1,
            Properties: [
                { PropertyName: 'color', PropertyValue: tile.color },
                { PropertyName: 'width', PropertyValue: parseInt(tile.width) },
                { PropertyName: 'height', PropertyValue: parseInt(tile.height) },
                { PropertyName: 'title', PropertyValue: tile.title },
                { PropertyName: 'type', PropertyValue: tile.type },
                { PropertyName: 'dataSource', PropertyValue: tile.dataSource },
                { PropertyName: 'columns', PropertyValue: JSON.stringify(tile.columns) },
                { PropertyName: 'value', PropertyValue: JSON.stringify(tile.value) },
                { PropertyName: 'label', PropertyValue: tile.label },
                { PropertyName: 'link', PropertyValue: tile.link },
                { PropertyName: 'format', PropertyValue: tile.format }
            ]
        };
        Dashboard.Tiles.push(Tile);
    };

    var request = $http({
        method: "POST",
        url: "/Dashboard/SaveDashboard",
        data: JSON.stringify(Dashboard),
        headers : {
            'Content-Type': 'application/json'
        }

    });

    return (request.then(handleSuccess, handleError));
};

DataService.saveDashboard

In the MVC SaveDashboard action below, the Dashboard object now has an IsDefault bool flag. When SaveDashboard is called, it's either for the traditional purpose of saving the user's custom dashboard layout (IsDefault=false), or for saving a new default dashboard that applies to everyone (IsDefault=true). Lines 6-7 customize values used in INSERT database queries depending on what the target dashboard is. In line 222, the same thing happens for DeleteDashboard.
[HttpPost]
public void SaveDashboard(Dashboard dashboard)
{
    try
    {
        int priority = dashboard.IsDefault ? 1 : 2;
        String username = dashboard.IsDefault ? "default" : CurrentUsername();

        DeleteDashboard(dashboard.IsDefault);  // Delete prior saved dashboard (if any) for user.

        // Check whether an existing dashboard is saved for this user. If so, delete it.

        int dashboardId = -1;

        using (SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.AppSettings["Database"]))
        {
            conn.Open();

            // Add dashboard layout root record

            String query = "INSERT INTO DashboardLayout (DashboardName, Username, Priority) VALUES (@DashboardName, @Username, @priority); SELECT SCOPE_IDENTITY();";

            using (SqlCommand cmd = new SqlCommand(query, conn))
            {
                cmd.Parameters.AddWithValue("@DashboardName", "Home");
                cmd.Parameters.AddWithValue("@Username", username);
                cmd.Parameters.AddWithValue("@Priority", priority);
                using (SqlDataReader reader = cmd.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        dashboardId = Convert.ToInt32(reader[0]);
                    }
                }
            }

            if (dashboardId!=-1) // If root record added and we have an id, proceed to add child records
            {
                // Add DashboardLayoutTile records.

                int sequence = 1;
                foreach (Tile tile in dashboard.Tiles)
                {
                    query = "INSERT INTO DashboardLayoutTile (DashboardId, Sequence) VALUES (@DashboardId, @Sequence)";

                    using (SqlCommand cmd = new SqlCommand(query, conn))
                    {
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.ExecuteNonQuery();
                    }
                    sequence++;
                } // next tile

                // Add DashboardLayoutTileProperty records.

                sequence = 1;
                foreach (Tile tile in dashboard.Tiles)
                {
                    query = "INSERT INTO DashboardLayoutTileProperty (DashboardId, Sequence, PropertyName, PropertyValue) VALUES (@DashboardId, @Sequence, @Name, @Value)";

                    using (SqlCommand cmd = new SqlCommand(query, conn))
                    {
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "color");
                        if (tile["color"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["color"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "height");
                        if (tile["height"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["height"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "width");
                        if (tile["width"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["width"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "type");
                        if (tile["type"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["type"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "title");
                        if (tile["title"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["title"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "dataSource");
                        if (tile["dataSource"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["dataSource"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "label");
                        if (tile["label"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["label"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "columns");
                        if (tile["columns"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["columns"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "value");
                        if (tile["value"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["value"]);
                        }
                        cmd.ExecuteNonQuery();

                        cmd.Parameters.Clear();
                        cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                        cmd.Parameters.AddWithValue("@Sequence", sequence);
                        cmd.Parameters.AddWithValue("@Name", "link");
                        if (tile["link"] == null)
                        {
                            cmd.Parameters.AddWithValue("@Value", DBNull.Value);
                        }
                        else
                        {
                            cmd.Parameters.AddWithValue("@Value", tile["link"]);
                        }
                        cmd.ExecuteNonQuery();
                    }
                    sequence++;
                } // next tile
            }

            conn.Close();
        } // end SqlConnection
    }
    catch(Exception ex)
    {
        Console.WriteLine("EXCEPTION: " + ex.Message);
    }
}

// DeleteDashboard : Delete user's saved custom dashboard. If isDefault is true, deletes the default dashboard.

private void DeleteDashboard(bool isDefault)
{
    try
    {
        String username = isDefault ? "default" : CurrentUsername();

        // Check whether an existing dashboard is saved for this user. If so, delete it.

        int dashboardId = -1;

        using (SqlConnection conn = new SqlConnection(System.Configuration.ConfigurationManager.AppSettings["Database"]))
        {
            conn.Open();

            // Load the dashboard.
            // If the user has a saved dashboard, load that. Otherwise laod the default dashboard.

            String query = "SELECT TOP 1 DashboardId FROM DashboardLayout WHERE DashboardName='Home' AND Username=@Username";

            using (SqlCommand cmd = new SqlCommand(query, conn))
            {
                cmd.CommandType = System.Data.CommandType.Text;
                cmd.Parameters.AddWithValue("@Username", username);
                using (SqlDataReader reader = cmd.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        dashboardId = Convert.ToInt32(reader["DashboardId"]);
                    }
                }
            }

            if (dashboardId != -1) // If found a dashboard...
            {
                // Delete dashboard layout tile property records

                query = "DELETE DashboardLayoutTileProperty WHERE DashboardId=@DashboardId";

                using (SqlCommand cmd = new SqlCommand(query, conn))
                {
                    cmd.CommandType = System.Data.CommandType.Text;
                    cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                    cmd.ExecuteNonQuery();
                }

                // Delete dashboard layout tile records

                query = "DELETE DashboardLayoutTile WHERE DashboardId=@DashboardId";

                using (SqlCommand cmd = new SqlCommand(query, conn))
                {
                    cmd.CommandType = System.Data.CommandType.Text;
                    cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                    cmd.ExecuteNonQuery();
                }

                // Delete dashboard layout record

                query = "DELETE DashboardLayout WHERE DashboardId=@DashboardId";

                using (SqlCommand cmd = new SqlCommand(query, conn))
                {
                    cmd.CommandType = System.Data.CommandType.Text;
                    cmd.Parameters.AddWithValue("@DashboardId", dashboardId);
                    cmd.ExecuteNonQuery();
                }
            }
            conn.Close();
        } // end SqlConnection
    }
    catch (Exception ex)
    {
        Console.WriteLine("EXCEPTION: " + ex.Message);
    }
}

MVC DashboardController SaveDashboard Action

This takes care of the functionality for Make Default Layout, except for one other matter: we don't want just any user to be able to set the default layout--that should be reserved for administrators.

Reserving Make Default Layout for Administrators

In order to enforce Make Default Layout only being avaiable for administrators, we need to know whether our user is an administrator. In the MVC DashboardController, our pre-existing CurrentUsername function is now accompanied with a CurrentUserIsAdmin function. In the demo project, the return values are hard-coded; in your real application, your authentication/authorization mechanism would be used to determine identity and privileges.

#region Authentication and Authorization

// Use these methods to simulate different users, by changing the username or admin privilege. In a real app, your authN/authZ system handles this.

// Return current username. Since this is just a demo project that lacks authentication, a hard-coded username is returned.

private String CurrentUsername()
{
    //return "Mike.Jones";
    //return "Karen.Carpenter";
    return "John.Smith";
}

// Return true if current user is an administrator. Since this is just a demo project that lacks authentication, a hard-coded role assignment is made.

private bool CurrentUserIsAdmin()
{
    switch (this.CurrentUsername())
    {
        case "John.Smith":
            return true;
        default:
            return false;
    }
}

#endregion
MVC Dashboard Methods for Username and Administrator Status

We need to pass the user information on to the Angular side of things. This is done with a new MVC action named GetUser. 
// /Dashboard/GetUser .... returns username and admin privilege of cucrrent user.

[HttpGet]
public JsonResult GetUser()
{
    User user = new User()
    {
        Username = CurrentUsername(),
        IsAdmin = CurrentUserIsAdmin()
    };
    return Json(user, JsonRequestBehavior.AllowGet);
}
MVC Dashboard GetUser Action

There is a matching getUser function in the DataService that the controller uses to get this information.
// -------------------- getUser : return user information.

self.getUser = function () {

    var url = '/Dashboard/GetUser';

    var request = $http({
        method: "GET",
        url: url,
    });

    return (request.then(getUser_handleSuccess, handleError));
};
DataService.getUser function

The HTML template uses ng-if to conditionally show the Make Default Layout menu option: it only appears if the current user is an administrator.
<div class="hovermenu">
    <i class="fa fa-ellipsis-h dropdown-toggle" data-toggle="dropdown" aria-hidden="true"></i>
    <ul class="dropdown-menu" style="margin-left:-150px !important">
        <li><a id="tile-config-{{tile.id}}" href="#" onclick="configureTile(this.id);"><i class="fa fa-gear" aria-hidden="true"></i>  Configure Tile</a></li>
        <li><a href="#" onclick="configureTile('0');"><i class="fa fa-plus-square-o" aria-hidden="true"></i>  Add Tile</a></li>
        <li><a id=tile-remove-{{tile.id}}" href="#" onclick="removeTileConfirm(this.id);"><i class="fa fa-trash-o" aria-hidden="true"></i>  Remove Tile</a></li>
        <li><a id=tile-reset-{{tile.id}}" href="#" onclick="resetDashboardConfirm();"><i class="fa fa-refresh" aria-hidden="true"></i>  Reset Dashboard</a></li>
        <li ng-if="$ctrl.user.IsAdmin"><a id=tile-reset-{{tile.id}}" href="#" onclick="saveDefaultDashboardConfirm();"><i class="fa fa-check-square-o" aria-hidden="true"></i>  Make Default Layout</a></li>
    </ul>
</div>
Use of ng-if to show menu option for administrator users


Improving the Table Tile

Our Table tile works well enough, but it's kind of crammed. It could do with an expanded layout, with more space padding in the cells. But doing that will make it even harder for the table content to fit in the tile space at times--it really needs a horizontal scroll bar.

In our dashboard.less file, we've updated the styles for table tbody elements to scroll horizontally when necessary; and added greated padding for table cells.
.tile tbody {
    color: black;
    background-color: white;
    height: 100%;
    overflow-y: auto;    /* vertical scroll when needed */
    overflow-x: auto;    /* horizontal scroll when needed */
    font-size: 12px;
}

.tile td, .tile th {
    padding: 8px;
}
Updated table styles

Our improved tile markup is shown below. 
<!-- TABLE tile -->
<div ng-if="tile.type=='table'"
        style="text-align: left !important; padding: 16px; height: 100%">
    <div style="height: 100%; text-align: left !important">
        <table style="padding-bottom: 28px;">
            <tbody style="max-width: {{$ctrl.tileTableWidth(tile.id); }}">
                <tr>
                    <th ng-repeat="col in tile.columns">{{col[0]}}</th>
                </tr>
                <tr ng-repeat="row in tile.value">
                    <td ng-repeat="cell in row track by $index">
                        <div ng-if="tile.columns[$index][1]=='number'" class="td-right">{{cell}}</div>
                        <div ng-if="tile.columns[$index][1]!='number'">{{cell}}</div>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
</div>
Updated tile markup

With the above changes, our table tile is much improved: it's more readable, and the content is scrollable.

Improved Table Tile

Summary


Today we did the following to polish and refine our earlier efforts:

  • Added confirmation dialogs for several tile actions.
  • Added a new tile action, Make Default Layout, for administrators.
  • Passed user information from the back end to the front end, including admin role.
  • Improved the Table tile's appearance and made it scrollable.
  • Released updated code with bug fixes.
Download Source
Download Zip
https://drive.google.com/open?id=1913tZaEmxSFj9StyMlKCynePpLw43T0O

No comments: