Creating Default Widgets using Roles

August 23rd, 2024

In today's post, we'll continue to use Identity to create widgets for specific roles

What's Next?

Full examples located at Tuxboard.Examples.

In the last post, we created default dashboards for specific roles. When a new user is added, default dashboards make the onboarding easier for a user.

One feature of Tuxboard is the ability to deliver role-specific widgets. Role-based widgets can be administrative, standard, or simple informative.

In today's post, we'll demonstrate how to leverage those roles in delivering role-specific widgets to users. The widgets are filtered on the server and delivered to the user through the Add Widgets dialog. The good news for backend developers is no client-side code is necessary to update.

The process is similar to the previous post of creating default dashboards, but at a widget level.

Getting Started

The project we'll be working with is located at Tuxboard.Examples called 11-Default-Widgets.

Again, we need to setup the project so we can see how it runs and then follow along of how the project was built.

To get started with the finished project,

  1. Right-click on the package.json file and Restore Packages so the client-side JavaScript/TypeScript will work as expected.
  2. Open the appsettings.json file and update the connection string.
  3. Open the Package Manager Console (View > Other Windows... > Package Manager Console)
  4. Confirm you have the 11-Default-Widgets project selected in the Default Project Dropdown in the Package Manager Console.
  5. Since we already have a migration in place, type update-database -Context TuxboardRoleDbContext

After updating the database, users and their roles are the only things missing to complete this demonstration.

As mentioned in the post before under the "Configuring the Database" section, register your users and assign each one a role before moving forward.

Creating the WidgetRole Entity

The WidgetRole entity is meant to be an associative (or junction) table. The entity is defined as follows:

public class WidgetRole
{
    public virtual Guid WidgetId { get; set; }
    public virtual Guid RoleId { get; set; }

    public virtual Widget Widget { get; set; } = default!;     public virtual TuxboardRole Role { get; set; } = default!; }

Once we have the entity, we need to add our WidgetRole to the TuxboardRoleDbContext.

First, we need a WidgetRoleConfiguration in our Data\Configuration directory.

public class WidgetRoleConfiguration: IEntityTypeConfiguration<WidgetRole>
{
    public void Configure(EntityTypeBuilder<WidgetRole> builder)
    {
        builder.ToTable(nameof(WidgetRole));

        builder.HasKey(r => new { r.WidgetId, r.RoleId });     } }

Next, we need to add a DbSet to the TuxboardRoleDbContext. First, through the interface (changes in bold).

public interface ITuxboardRoleDbContext: ITuxDbContext
{
    DbSet<RoleDefaultDashboard> RoleDefaultDashboards { get; set; }
    DbSet<WidgetRole> WidgetRoles { get; set; }
.
.

Then, we add the DbSet<WidgetRole> to our concrete class TuxboardRoleDbContext.

public class TuxboardRoleDbContext : TuxDbContext, ITuxboardRoleDbContext
{
    public TuxboardRoleDbContext(DbContextOptions<TuxDbContext> options, IOptions<TuxboardConfig> config)
        : base(options, config)
    {
    }

    public DbSet<RoleDefaultDashboard> RoleDefaultDashboards { get; set; }

   public DbSet<WidgetRole> WidgetRoles { get; set; } .
.    

In the OnModelCreating, don't forget to add the WidgetRoleConfiguration().

modelBuilder.ApplyConfiguration(new WidgetRoleConfiguration());

Creating the Service

Since we have the WidgetRole table, our focus is now the service and how to pull the widgets based on a role.

The interface is meant to be as simple as the RoleDashboardService from before.

public interface IWidgetRoleService
{
    Task<List<Widget>> GetWidgetsByRoleAsync(TuxboardUser user);
    Task<List<Widget>> GetDefaultWidgetsAsync();
}

The GetWidgetsByRoleAsync() retrieves the widgets based on a user's role, but what about the GetDefaultWidgetsAsync()? This concept is similar to how a user logs in and is given either a role-specific dashboard or a default dashboard. If they're a registered user, they should receive a dashboard.

The same concept applies to widgets. If they are a registered user, but don't have a role, they should receive a collection of widgets to add to their dashboard.

public class WidgetRoleService : IWidgetRoleService
{
    private readonly ITuxboardRoleDbContext _context;
    private readonly UserManager<TuxboardUser> _userManager;
    private readonly RoleManager<TuxboardRole> _roleManager;

    public WidgetRoleService(ITuxboardRoleDbContext context,         UserManager<TuxboardUser> userManager,         RoleManager<TuxboardRole> roleManager)     {         _context = context;         _userManager = userManager;         _roleManager = roleManager;     }
    public async Task<List<Widget>> GetWidgetsByRoleAsync(TuxboardUser user)     {         // Give them something at least.         var result = await GetDefaultWidgetsAsync();
        var roleName = await GetRoles(user);         if (string.IsNullOrEmpty(roleName))         {             return result;         }
        var role = await _roleManager.FindByNameAsync(roleName);         if (role == null)             return result;
        return await _context.WidgetRoles             .Include(e=> e.Widget)             .Where(e => e.RoleId == role.Id)             .Select(r=> r.Widget)             .ToListAsync();     }
    public async Task<List<Widget>> GetDefaultWidgetsAsync() =>         // Set up your own GroupName like "Standard" or something.         await _context.Widgets             .Where(e => e.GroupName == "Example")              .ToListAsync();
    private async Task<string> GetRoles(TuxboardUser user)     {         // *COULD* have more than one role; we just want the first one.         var roles = await _userManager.GetRolesAsync(user);         return (roles.Count == 1             ? roles.FirstOrDefault()             : string.Empty)!;     } }

The GetWidgetsByRoleAsync() takes a TuxboardUser and immediately retrieves the default widgets for unregistered users or users without a role. In this case, widgets in the GroupName called "Example" are the default widgets presented to the user.

If they're a registered user and have a role, then the user is presented with a list of role-specific widgets in the dialog.

With the list of role-specific widgets, we can move up a level to the Add Widgets dialog in our Index page.

Dependency Injecting the WidgetRoleService

Before we head over to the Index page, we need to add our new service to our Middleware in the Program.cs.

builder.Services.AddTransient<IWidgetRoleService, WidgetRoleService>();

Once we update our Program.cs, we can move on to the Index page.

Updating the Add Widget Dialog

In the Index page, we need to inject our WidgetRoleService through the constructor (changes in bold).

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    private readonly IDashboardService _service;
    private readonly IRoleDashboardService _roleDashboardService;
    private readonly IWidgetRoleService _widgetRoleService;
    private readonly UserManager<TuxboardUser> _userManager;
    private readonly TuxboardConfig _config;

    public Dashboard Dashboard { get; set; } = null!;     public bool HasDashboard => Dashboard != null;
    public IndexModel(         ILogger<IndexModel> logger,         IDashboardService service,         IRoleDashboardService roleDashboardService,         IWidgetRoleService widgetRoleService,         UserManager<TuxboardUser> userManager,         IOptions<TuxboardConfig> options)     {         _logger = logger;         _service = service;         _roleDashboardService = roleDashboardService;         _widgetRoleService = widgetRoleService;         _userManager = userManager;         _config = options.Value;     }
.
.

Once we're able to inject the service into the Index page, the OnPostAddWidgetsDialog() method is easier to implement.

public async Task<IActionResult> OnPostAddWidgetsDialog()
{
    List<WidgetDto> widgets = new();

    var id = GetIdentity();     if (id != Guid.Empty)     {         var user = await GetTuxboardUser(id);         widgets.AddRange(             (await _widgetRoleService.GetWidgetsByRoleAsync(user))             .Select(r => r.ToDto())             .ToList()         );     }     else     {         widgets.AddRange(             (await _widgetRoleService.GetDefaultWidgetsAsync())             .Select(r => r.ToDto())             .ToList()         );     }
   return ViewComponent("addwidgetdialog", new AddWidgetModel { Widgets = widgets }); }

Let's walk through the method.

We initialize the list of widgets to return to the Add Widgets Dialog as empty (for now).

The GetIdentity() retrieves the current user logged in.

Since we're retrieving the user in multiple places throughout the code, it made sense to create a new method to grab a TuxboardUser.

private async Task<TuxboardUser> GetTuxboardUser(Guid id) 
    => (await _userManager.FindByIdAsync(id.ToString()))!;

If the user is logged in, get the widgets by the user's role. If they aren't logged in, return a list of default widgets. Whether logged in or not, we receive a list of widgets and convert them into DTOs (Data Transfer Objects) for our Add Widget Dialog.

The good news is we simply modified the way a user retrieves widgets based on their roles. Once we have the widget DTOs, we pass them on to the AddWidgetDialogViewComponent to render and send the view back to the client.

Updating the Database

The best way to present widgets to users is to take a hard look at the roles and identify which widgets are meant for privileged users and standard users.

If all users are meant to have any widget, insert all of the widgets and roles through SQL.

INSERT INTO WidgetRole
SELECT
    w.WidgetId,
    tr.Id as RoleId
FROM Widget w
join TuxboardRole tr on 1=1

The SQL above will add all widgets to every role.

Once all WidgetRoles are entered into the table, double-check the table by using the following SQL statement.

SELECT
   tr.Name,
   w.Name,
   w.Title,
   w.GroupName
FROM WidgetRole wr
join Widget w on w.WidgetId=wr.WidgetId
join TuxboardRole tr on tr.Id=wr.RoleId
Role Name Title GroupName
Basic helloworld Hello World Example
Admin table Sample Table General
Admin generalinfo General Info General
Admin helloworld Hello World Example

The SQL results are meant to show an easy view of the roles and their associated widgets. In this example, basic users are only able to add a Hello World widget, but administrators are able to add all widgets.

Viewing The Results

When we run the application and log in as an administrator and use the Add Widget dialog, we can see the following results.

However, if we log in as a basic user and want to add a widget to the dashboard, our widget list is limited.

Providing specific widgets based on a user's role demonstrates Tuxboard's unique approach to dashboards.

Conclusion

While we focused on role-based widgets and default dashboards, the goal of these two past posts were meant to show the flexibility of Tuxboard and how to expand on it's ability to adapt to other concepts, but still keep the dashboard robust and maintainable.

The original concept was to introduce subscriber plans to Tuxboard for the initial design. The Plan and WidgetPlan table along with the DashboardDefault's PlanID field was created for inspiring developer/entrepreneurs to integrate a consumer-based dashboard into a product. They were originally intended for subscriber plans. This was touched on in the last post (under "Configuring the Database").

Based on these two posts, we were able to demonstrate how Tuxboard uses a role-based approach as easily as a subscriber plan approach.

What's Next?

Full examples located at Tuxboard.Examples.