Creating Default Dashboards using Roles
Today, we demonstrate how to create default dashboards in Tuxboard based on a user's role
What's Next?
- Introducing Tuxboard
- Layout of a Tuxboard dashboard
- Dashboard Modularity using Tuxboard
- Moving Widgets in Tuxboard
- Creating a Tuxbar for Tuxboard
- Managing Layouts in Tuxboard: Simple Layout Dialog
- Managing Layouts in Tuxboard: Advanced Layout Dialog
- Adding Widgets with a Tuxboard Dialog
- Using Widget Toolbars (or Deleting Widgets)
- Creating User-Specific Dashboards
- Creating Default Dashboards using Roles
- Creating Default Widgets using Roles
- Creating Custom Tuxboard Widgets
Full examples located at Tuxboard.Examples.
In the last post, we demonstrated how to create user-specific dashboards for any user signing in and providing standard widgets for every user.
However, what if you have different user permissions? If we include administrative widgets in a standard user's dashboard, we may have some problems down the road.
In today's post, we'll look at how to create default dashboards for different roles using Microsoft Identity. While this technique is focusing on Microsoft Identity, it can easily be modified to work with other security frameworks like Microsoft Entra.
The example we'll review is in the 10-Default-Dashboards folder. The 10-Default-Dashboards project has already gone through all of these steps. All that's required to run is the initial setup and migrations.
Getting Started
As always, there is a repository available of everything we'll cover in this post at Tuxboard.Examples.
At first, we'll 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,
- Right-click on the
package.json
file and Restore Packages so the client-side JavaScript/TypeScript will work as expected. - Open the
appsettings.json
file and update the connection string. - Open the Package Manager Console (View > Other Windows... > Package Manager Console)
- Confirm you have the 10-Default-Dashboards project selected in the Default Project Dropdown in the Package Manager Console.
- 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.
If you'd like to fast-forward to creating the users and roles, jump down to the "Configuring the Database" section to successfully run the project.
With that said, we can now review the project to understand how to create default dashboards in our own projects.
Understanding Default Dashboards
Default dashboards are meant to give a specific dashboard and/or collection of widgets to different types of users. If a user logged into a website and were given a blank screen, it wouldn't be a very good experience for the user, would it?
Since we only have a single widget/default dashboard available, we may want to add a dashboard for new users with a tutorial widget or a Getting Started widget. If they're an administrator, they would want to see a statistics widget on how many users logged in for the day, what notifications happened since the last log in, or even a to-do list.
In the database, the tables to focus on are the DefaultDashboard and Layout tables.
The DefaultDashboard table provides a LayoutID and PlanID. Originally, the Tuxboard schema was meant for subscription plans (DefaultDashboard table) and widgets available based on a plan (WidgetPlan table). Since we're using roles with Identity, we won't be using the PlanID field in the DefaultDashboard table or the WidgetPlan table.
The Layout table is what's important. If there is a null in the TabID, it's a Default Dashboard layout of some kind. Every time a user logs in, they're given a TabID for their dashboard and a layout is assigned to the tab. Basically, an empty TabID means it doesn't belong to a user.
After running the update-database command, two default dashboard records are added and two layout records are added.
The last piece we need for this project is the Roles <-> DefaultDashboard relationship which we'll get into in a bit.
Adding Identity Models
Once we copied everything over from the user-specific dashboards project to create our new project, we're ready to use Microsoft Identity immediately.
Unfortunately, there is an issue with our schema: the DefaultDashboard table uses a GUID and the Roles table in the Identity schema uses a string by default so we need to adjust our Identity models to match what we need.
public class TuxboardUser : IdentityUser<Guid> public class TuxboardUserClaim : IdentityUserClaim<Guid> public class TuxboardUserLogin : IdentityUserLogin<Guid> public class TuxboardUserRole : IdentityUserRole<Guid> public class TuxboardUserToken : IdentityUserToken<Guid> public class TuxboardUserStore : UserStore<TuxboardUser, TuxboardRole, TuxboardRoleDbContext, Guid, TuxboardUserClaim, TuxboardUserRole, TuxboardUserLogin, TuxboardUserToken, TuxboardRoleClaim>
public class TuxboardRole : IdentityRole<Guid> public class TuxboardRoleClaim : IdentityRoleClaim<Guid> public class TuxboardRoleStore : RoleStore<TuxboardRole, TuxboardRoleDbContext, Guid, TuxboardUserRole, TuxboardRoleClaim>
These models were created based on the Identity post on learn.microsoft.com called Identity Model Customization. Based on the above class signatures, we need a TuxboardRoleDbContext
.
public interface ITuxboardRoleDbContext : ITuxDbContext { DbSet<RoleDefaultDashboard> RoleDefaultDashboards { get; set; }
// Identity DbSet<TuxboardUserClaim> TuxboardUserClaims { get; set; } DbSet<TuxboardUserRole> TuxboardUserRoles { get; set; } DbSet<TuxboardUserLogin> TuxboardUserLogins { get; set; } DbSet<TuxboardUserToken> TuxboardUserTokens { get; set; } DbSet<TuxboardUser> TuxboardUsers { get; set; } DbSet<TuxboardRole> TuxboardRoles { get; set; } DbSet<TuxboardRoleClaim> TuxboardRoleClaims { get; set; } }
public class TuxboardRoleDbContext : TuxDbContext, ITuxboardRoleDbContext { public TuxboardRoleDbContext(DbContextOptions<TuxDbContext> options, IOptions<TuxboardConfig> config) : base(options, config) { }
public DbSet<RoleDefaultDashboard> RoleDefaultDashboards { get; set; }
// Identity public DbSet<TuxboardUserClaim> TuxboardUserClaims { get; set; } public DbSet<TuxboardUserRole> TuxboardUserRoles { get; set; } public DbSet<TuxboardUserToken> TuxboardUserTokens { get; set; } public DbSet<TuxboardUserLogin> TuxboardUserLogins { get; set; } public DbSet<TuxboardUser> TuxboardUsers { get; set; } public DbSet<TuxboardRole> TuxboardRoles { get; set; } public DbSet<TuxboardRoleClaim> TuxboardRoleClaims { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new RoleDefaultDashboardConfiguration()); modelBuilder.ApplyConfiguration(new DashboardLayoutConfiguration()); modelBuilder.ApplyConfiguration(new DashboardLayoutRowConfiguration()); modelBuilder.ApplyConfiguration(new DashboardDefaultConfiguration()); modelBuilder.ApplyConfiguration(new DashboardDefaultWidgetConfiguration());
// Identity modelBuilder.ApplyConfiguration(new TuxboardRoleConfiguration()); modelBuilder.ApplyConfiguration(new TuxboardRoleClaimConfiguration()); modelBuilder.ApplyConfiguration(new TuxboardUserConfiguration()); modelBuilder.ApplyConfiguration(new TuxboardUserClaimConfiguration()); modelBuilder.ApplyConfiguration(new TuxboardUserLoginConfiguration()); modelBuilder.ApplyConfiguration(new TuxboardUserRoleConfiguration()); modelBuilder.ApplyConfiguration(new TuxboardUserTokenConfiguration()); } }
Even though the TuxboardRoleDbContext
is small, there is a lot to unpack here along with some interesting insights.
Multiple DbContexts into One
In regards to Entity Framework DbContexts, it's recommended to minimize the number of DbContext's used in an application. As a matter of fact, if you can get away with one DbContext, it's for the best. It also makes it easier to track entity state changes.
Since we already have a TuxDbContext
, how could we "attach" Identity models to the existing DbContext?
Simple. Add the DbSet
s and Configurations for each Identity models in the inherited DbContext.
One of the shockers (for me anyways) was the ability to attach Identity models to an existing DbContext and have it work as expected instead of having one DbContext
for TuxDbContext
and one for Identity.
Inheriting Aggregates Configurations
The configurations was another shock as to the power of Entity Framework Core.
For the standard Tuxboard configurations above, we are simply adding additional records to each table. While we already had Tuxboard configurations in the base TuxDbContext
class, we're adding more configurations for additional Layout and LayoutRow records.
When we did a migration of the new TuxboardRoleDbContext
, all configurations were combined into one making the records inserted as one batch.
For example, the DashboardLayoutConfiguration
class above consists of the following code.
public class DashboardLayoutConfiguration: IEntityTypeConfiguration<Layout> { public void Configure(EntityTypeBuilder<Layout> builder) { builder.HasData(new List<Layout> { new() { LayoutId = new Guid("239C89ED-3310-40D8-9104-237659415392"), TabId = null, LayoutIndex = 1 } }); } }
However, when the migration finished, the code consisted of the following code.
migrationBuilder.InsertData( schema: "dbo", table: "Layout", columns: new[] { "LayoutId", "LayoutIndex", "TabId" }, values: new object[,] { { new Guid("239c89ed-3310-40d8-9104-237659415392"), 1, null }, { new Guid("5267da05-afe4-4753-9cee-d5d32c2b068e"), 1, null } });
It used the .HasData()
method from both the TuxDbContext
and TuxboardRoleDbContext
.
Creating the Junction/Association Table
While integrating Identity into the existing TuxDbContext was a happy experience, we need to focus on our target: associating a role to a default dashboard.
For those who were paying attention, there was a DbSet
called RoleDefaultDashboards
in our new TuxboardRoleDbContext
. This is our junction table and the model is based on the following code.
public class RoleDefaultDashboard { public virtual Guid DefaultDashboardId { get; set; } public virtual Guid RoleId { get; set; }
public virtual DashboardDefault DefaultDashboard { get; set; } = default!; public virtual TuxboardRole Role { get; set; } = default!; }
The RoleDefaultDashboardConfiguration
class consists of the code below.
public class RoleDefaultDashboardConfiguration: IEntityTypeConfiguration<RoleDefaultDashboard> { public void Configure(EntityTypeBuilder<RoleDefaultDashboard> builder) { builder.HasKey(r => new { r.DefaultDashboardId, r.RoleId }); builder.HasData(new List<RoleDefaultDashboard> { new() { RoleId = new Guid("7E69EB1F-07C0-46A1-B4E8-86F56386C250"), // Admin DefaultDashboardId = new Guid("0D96A18E-90B8-4A9F-9DF1-126653D68FE6") // Admin Dashboard }, new() { RoleId = new Guid("31C3DF95-FDC6-4FB5-82AB-0436EA93C1B1"), // Basic DefaultDashboardId = new Guid("1623F469-D9F0-400C-8A4C-B4366233F485") // Basic dashboard } }); } }
One thing missing is how we load our dashboard based on a user's role...which brings us to our RoleDashboardService
.
public interface IRoleDashboardService { Task<DashboardDefault> GetDashboardTemplateByRoleAsync(TuxboardUser user); Task<bool> DashboardExistsForAsync(Guid userId); }
public class RoleDashboardService : IRoleDashboardService { private readonly ITuxboardRoleDbContext _context; private readonly UserManager<TuxboardUser> _userManager; private readonly RoleManager<TuxboardRole> _roleManager;
public RoleDashboardService(ITuxboardRoleDbContext context, UserManager<TuxboardUser> userManager, RoleManager<TuxboardRole> roleManager) { _context = context; _userManager = userManager; _roleManager = roleManager; }
public async Task<bool> DashboardExistsForAsync(Guid userId) { return await _context.DashboardExistsForAsync(userId); }
public async Task<DashboardDefault> GetDashboardTemplateByRoleAsync(TuxboardUser user) { DashboardDefault defaultDashboard = null!;
var roleName = await GetRoles(user); if (string.IsNullOrEmpty(roleName)) { defaultDashboard = await _context.GetDashboardTemplateForAsync(); }
var role = await _roleManager.FindByNameAsync(roleName); if (role == null) return defaultDashboard ?? await _context.GetDashboardTemplateForAsync();
var roleDashboard = await _context.RoleDefaultDashboards .FirstOrDefaultAsync(e => e.RoleId == role.Id); if (roleDashboard != null) { defaultDashboard = (await _context.GetDashboardDefaultAsync(roleDashboard.DefaultDashboardId)) ?? null!; }
return defaultDashboard ?? await _context.GetDashboardTemplateForAsync(); }
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 GetDashboardTemplateByRoleAsync()
method is what performs the heavy lifting for our service.
First, we check to see if the user is associated with any roles from Identity. If they don't have a role associated to them, we load the default dashboard.
Next, we located the role name. If we don't return a role, again, we load the default dashboard.
We, then, check to see if there is an existing dashboard associated with a role. If not, once again, we load the default dashboard. If there is a default dashboard assigned to a role, return it.
The concept behind this method is to always return a default dashboard.
Calling the RoleDashboardService
With our newly created RoleDashboardService
created, we can add it to our Index.cshtml.cs
file (changes in bold).
public class IndexModel : PageModel { private readonly ILogger<IndexModel> _logger; private readonly IDashboardService _service; private readonly IRoleDashboardService _roleDashboardService; 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, UserManager<TuxboardUser> userManager, IOptions<TuxboardConfig> options) { _logger = logger; _service = service; _roleDashboardService = roleDashboardService; _userManager = userManager; _config = options.Value; } public async Task OnGet() { var id = GetIdentity(); if (id != Guid.Empty) { Dashboard = await _service.DashboardExistsForAsync(id) ? await _service.GetDashboardForAsync(_config, id) : await GetDashboardByRole(id); } } private async Task<Dashboard> GetDashboardByRole(Guid id) { var user = await _userManager.FindByIdAsync(id.ToString()); // If we can't find the user, load the default dashboard. if (user == null) return await _service.GetDashboardAsync(_config); var template = await _roleDashboardService.GetDashboardTemplateByRoleAsync(user); await _service.CreateDashboardFromAsync(template, id); return await _service.GetDashboardForAsync(_config, id); }
.
.
While our OnGet()
contains a bit more code, our new service makes it easier to use.
If we have a user id and a dashboard exists for a user, assign the dashboard to the Dashboard property. If not, see if we can get a dashboard by a specific role.
The GetDashboardByRole
method takes in a GUID (user.ID) and tries to locate the user. If it can't find it, return a default dashboard through the GetDashboardAsync(_config)
method.
If we made it this far, we attempt to get a DefaultDashboard template by role. As mentioned previously, we ALWAYS return a DefaultDashboard since the next line uses the template to build a new dashboard for the user.
Finally, we return the users new dashboard.
Updating _LoginPartial.cshtml
Since we changed the Identity models, we have to update the _LoginPartial.cshtml
as well.
Change this:
@inject SignInManager<IdentityUser> SignInManager @inject UserManager<IdentityUser> UserManager
to this:
@inject SignInManager<TuxboardUser> SignInManager @inject UserManager<TuxboardUser> UserManager
With everything in place, we can now look at the tables and how to set this up.
Configuring the Database
As mentioned above, the Tuxboard database was originally meant for software products using the Plan
and WidgetPlan
tables. Example subscription plans would be similar to the following:
PlanID | Title |
---|---|
1 | Platinum |
2 | Gold |
3 | Silver |
4 | Bronze |
or
PlanID | Title |
---|---|
1 | Free |
2 | Premium |
3 | Professional |
4 | Enterprise |
The product can contain any number of marketing tiers or levels for different experience levels of users.
When a user signs up, they select a Plan based on their needs. The PlanID is attached to a default dashboard which is set up ahead of time.
If you've jumped down to here from the top, we need a way to attach these roles to the users.
The problem with a roles approach is identifying the role of a user when they register. How do we know a user's role when they get to their dashboard? There needs to be a source of truth for the roles (and I leave that to the administrators of the system).
When a user registers on a website, they can tell you what subscription plan they want which is why the Plan table is a better for consumers.
For right now, we'll proceed with updating the database by showing you where to hit to make this work as expected.
Creating the Records
For this process, we need to create two users with passwords.
- Open SQL Server Management Studio (SSMS) and locate the 10-Default-Dashboards database. The database should already be created based on the "update-database" command performed at the beginning of the post.
- Open a new Query Window
- Type
SELECT * FROM TuxboardRole
There should be two records: Basic and Admin. Remember these two IDs for our UserRole table below. - Type
SELECT * FROM RoleDefaultDashboards
There should also be two records in there: one for Basic and one for Admin. - In Visual Studio, run the application and create two users.
- In SSMS, type
SELECT * FROM TuxboardUser
to show the two users. - To relate each user to a basic role, type the following and press F5 to execute it:
INSERT INTO TuxboardUserRole
SELECT
'<userid-from-tuxboarduser>' as UserId,
'31C3DF95-FDC6-4FB5-82AB-0436EA93C1B1' as RoleId -- Basic Role - For admin users, type in the following and press F5 to execute:
INSERT INTO TuxboardUserRole
SELECT
'<userid-from-tuxboarduser>' as UserId,
'7E69EB1F-07C0-46A1-B4E8-86F56386C250' as RoleId -- Admin Role
When you run the application and an administrator logs in, they'll be presented with an Administrative dashboard and a widget in the first column. When a regular user logs in, they'll see a standard dashboard with a widget in the right column.
Again, when a user logs in, they require a role to receive a dashboard. If they don't have a role assigned to them, they'll receive the default dashboard.
Conclusion
In this post, we assigned roles to default dashboards allowing users who login can see their own dashboard with custom widgets specific to their role.
We'll look at a better way to create these default dashboards in a future post.
What's Next?
- Introducing Tuxboard
- Layout of a Tuxboard dashboard
- Dashboard Modularity using Tuxboard
- Moving Widgets in Tuxboard
- Creating a Tuxbar for Tuxboard
- Managing Layouts in Tuxboard: Simple Layout Dialog
- Managing Layouts in Tuxboard: Advanced Layout Dialog
- Adding Widgets with a Tuxboard Dialog
- Using Widget Toolbars (or Deleting Widgets)
- Creating User-Specific Dashboards
- Creating Default Dashboards using Roles
- Creating Default Widgets using Roles
- Creating Custom Tuxboard Widgets
Full examples located at Tuxboard.Examples.