diff --git a/.idea/.idea.HopFrame/.idea/workspace.xml b/.idea/.idea.HopFrame/.idea/workspace.xml
index a488f67..9b21c57 100644
--- a/.idea/.idea.HopFrame/.idea/workspace.xml
+++ b/.idea/.idea.HopFrame/.idea/workspace.xml
@@ -11,8 +11,15 @@
-
+
+
+
+
+
+
+
+
@@ -32,7 +39,7 @@
@@ -59,29 +66,33 @@
+
+
+
+
+
-
{}
{
@@ -100,28 +111,28 @@
- {
+ "keyToString": {
+ ".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run",
+ ".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
+ ".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
+ ".NET Project.HopFrame.Testing.executor": "Run",
+ "72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
+ "dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
+ "git-widget-placeholder": "!29 on feature/custom-views",
+ "list.type.of.created.stylesheet": "CSS",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "settings.editor.selected.configurable": "preferences.environmentSetup",
+ "vue.rearranger.settings.migration": "true"
}
-}]]>
+}
@@ -220,7 +231,8 @@
-
+
+
@@ -494,7 +506,15 @@
1738407061976
-
+
+
+ 1738407710507
+
+
+
+ 1738407710507
+
+
@@ -545,7 +565,6 @@
-
@@ -570,6 +589,7 @@
-
+
+
\ No newline at end of file
diff --git a/src/HopFrame.Web/Components/HopFrameCard.razor b/src/HopFrame.Web/Components/HopFrameCard.razor
new file mode 100644
index 0000000..cfe6f93
--- /dev/null
+++ b/src/HopFrame.Web/Components/HopFrameCard.razor
@@ -0,0 +1,35 @@
+
+
+ @if (Icon is not null) {
+
+ }
+ @Title
+
+ @Subtitle
+ @Description
+
+
+
+
+@code {
+ [Parameter]
+ public required string Title { get; set; }
+
+ [Parameter]
+ public string? Subtitle { get; set; }
+
+ [Parameter]
+ public required string Description { get; set; }
+
+ [Parameter]
+ public required string Href { get; set; }
+
+ [Parameter]
+ public Icon? Icon { get; set; }
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web/Components/Layout/HopFrameLayout.razor b/src/HopFrame.Web/Components/Layout/HopFrameLayout.razor
index c9d3fa4..5be0a6b 100644
--- a/src/HopFrame.Web/Components/Layout/HopFrameLayout.razor
+++ b/src/HopFrame.Web/Components/Layout/HopFrameLayout.razor
@@ -1,5 +1,6 @@
@using HopFrame.Core.Config
@using HopFrame.Core.Services
+@using HopFrame.Web.Models
@using Microsoft.Extensions.DependencyInjection
@inherits LayoutComponentBase
@@ -39,10 +40,28 @@
@code {
+ internal static readonly List CustomViews = new();
+
protected override async Task OnInitializedAsync() {
var authorized = await Handler.IsAuthenticatedAsync(Config.BasePolicy);
+
+ var currentUri = "/" + Navigator.ToBaseRelativePath(Navigator.Uri);
+
+ if (authorized) {
+ foreach (var view in CustomViews.Where(view => !string.IsNullOrWhiteSpace(view.Policy))) {
+ switch (view.LinkMatch) {
+ case NavLinkMatch.All when currentUri != view.Url:
+ case NavLinkMatch.Prefix when !currentUri.StartsWith(view.Url):
+ continue;
+ }
+
+ authorized = await Handler.IsAuthenticatedAsync(view.Policy);
+ break;
+ }
+ }
+
if (!authorized) {
- Navigator.NavigateTo((Config.LoginPageRewrite ?? "/login") + "?redirect=/" + Navigator.ToBaseRelativePath(Navigator.Uri), true);
+ Navigator.NavigateTo((Config.LoginPageRewrite ?? "/login") + "?redirect=" + currentUri, true);
}
}
diff --git a/src/HopFrame.Web/Components/Layout/HopFrameSideMenu.razor b/src/HopFrame.Web/Components/Layout/HopFrameSideMenu.razor
index d839091..4b052fc 100644
--- a/src/HopFrame.Web/Components/Layout/HopFrameSideMenu.razor
+++ b/src/HopFrame.Web/Components/Layout/HopFrameSideMenu.razor
@@ -1,5 +1,6 @@
@using HopFrame.Core.Config
@using HopFrame.Core.Services
+@using HopFrame.Web.Models
+ @foreach (var view in _views) {
+
+ }
+
@foreach (var table in _tables.OrderBy(t => t.Order).Select(t => t.DisplayName)) {
_tables = [];
+ private readonly List _views = [];
protected override async Task OnInitializedAsync() {
foreach (var table in Explorer.GetTables()) {
@@ -34,6 +45,21 @@
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
_tables.Add(table);
}
+
+ foreach (var view in HopFrameLayout.CustomViews) {
+ if (!await Handler.IsAuthenticatedAsync(view.Policy)) continue;
+ _views.Add(view);
+ }
+ }
+
+ internal static Icon GetLinkIcon(CustomView view, IconVariant variant) {
+ var info = new IconInfo {
+ Name = view.Icon,
+ Variant = variant,
+ Size = IconSize.Size24
+ };
+
+ return info.GetInstance();
}
}
diff --git a/src/HopFrame.Web/Components/Pages/HopFrameHome.razor b/src/HopFrame.Web/Components/Pages/HopFrameHome.razor
index 6be85aa..137858f 100644
--- a/src/HopFrame.Web/Components/Pages/HopFrameHome.razor
+++ b/src/HopFrame.Web/Components/Pages/HopFrameHome.razor
@@ -1,28 +1,31 @@
@page "/admin"
@using HopFrame.Core.Config
@using HopFrame.Core.Services
+@using HopFrame.Web.Models
@layout HopFrameLayout
HopFrame
-
Tables
+
Pages
+ @foreach (var view in _views) {
+
+ }
+
@foreach (var table in _tables.OrderBy(t => t.Order)) {
-
- @table.DisplayName
- @table.ViewPolicy
- @table.Description
-
-
-
+
}
@@ -33,6 +36,7 @@
@code {
private readonly List _tables = [];
+ private readonly List _views = [];
protected override async Task OnInitializedAsync() {
foreach (var table in Explorer.GetTables()) {
@@ -40,6 +44,11 @@
if (!await Handler.IsAuthenticatedAsync(table.ViewPolicy)) continue;
_tables.Add(table);
}
+
+ foreach (var view in HopFrameLayout.CustomViews) {
+ if (!await Handler.IsAuthenticatedAsync(view.Policy)) continue;
+ _views.Add(view);
+ }
}
}
\ No newline at end of file
diff --git a/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs b/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs
new file mode 100644
index 0000000..3705ce3
--- /dev/null
+++ b/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs
@@ -0,0 +1,32 @@
+using HopFrame.Core.Config;
+using HopFrame.Web.Components.Layout;
+using HopFrame.Web.Models;
+
+namespace HopFrame.Web;
+
+public static class HopFrameConfiguratorExtensions {
+
+ ///
+ /// Creates an entry to the side menu and dashboard with a custom url
+ ///
+ /// The configurator for the HopFrame config that is being created
+ /// The name of the navigation entry
+ /// The target url of the navigation entry
+ public static CustomViewConfigurator AddCustomView(this HopFrameConfigurator configurator, string name, string url) {
+ var view = new CustomView {
+ Name = name,
+ Url = url
+ };
+ HopFrameLayout.CustomViews.Add(view);
+ return new CustomViewConfigurator(view);
+ }
+
+ /// The delegate for configuring the view
+ ///
+ public static HopFrameConfigurator AddCustomView(this HopFrameConfigurator configurator, string name, string url, Action configuratorDelegate) {
+ var viewConfigurator = AddCustomView(configurator, name, url);
+ configuratorDelegate.Invoke(viewConfigurator);
+ return configurator;
+ }
+
+}
\ No newline at end of file
diff --git a/src/HopFrame.Web/Models/CustomView.cs b/src/HopFrame.Web/Models/CustomView.cs
new file mode 100644
index 0000000..7ec4f83
--- /dev/null
+++ b/src/HopFrame.Web/Models/CustomView.cs
@@ -0,0 +1,54 @@
+using Microsoft.AspNetCore.Components.Routing;
+using Microsoft.FluentUI.AspNetCore.Components;
+
+namespace HopFrame.Web.Models;
+
+public sealed class CustomView {
+ public required string Name { get; init; }
+ public string? Description { get; set; }
+ public string? Policy { get; set; }
+ public required string Url { get; init; }
+ public string Icon { get; set; } = "Window";
+ public NavLinkMatch LinkMatch { get; set; } = NavLinkMatch.All;
+}
+
+public sealed class CustomViewConfigurator(CustomView view) {
+ public CustomView InnerConfig { get; } = view;
+
+ ///
+ /// Sets the description displayed in the dashboard
+ ///
+ /// The desired description
+ public CustomViewConfigurator SetDescription(string description) {
+ InnerConfig.Description = description;
+ return this;
+ }
+
+ ///
+ /// Sets the policy needed in order to access the view
+ ///
+ /// The desired policy
+ public CustomViewConfigurator SetPolicy(string policy) {
+ InnerConfig.Policy = policy;
+ return this;
+ }
+
+ ///
+ /// Sets the icon displayed in the sidebar
+ ///
+ /// The desired fluent-icon
+ public CustomViewConfigurator SetIcon(string icon) {
+ InnerConfig.Icon = icon;
+ return this;
+ }
+
+ ///
+ /// Sets the rule for sidebar to determine if the link is active
+ ///
+ /// The desired match rule
+ public CustomViewConfigurator SetLinkMatch(NavLinkMatch match) {
+ InnerConfig.LinkMatch = match;
+ return this;
+ }
+
+}
diff --git a/testing/HopFrame.Testing/Components/Pages/Counter.razor b/testing/HopFrame.Testing/Components/Pages/Counter.razor
index e4f070e..21517ec 100644
--- a/testing/HopFrame.Testing/Components/Pages/Counter.razor
+++ b/testing/HopFrame.Testing/Components/Pages/Counter.razor
@@ -1,5 +1,7 @@
@page "/counter"
+@using HopFrame.Web.Components.Layout
@rendermode InteractiveServer
+@layout HopFrameLayout
Counter
diff --git a/testing/HopFrame.Testing/Program.cs b/testing/HopFrame.Testing/Program.cs
index 97d1ad8..843462a 100644
--- a/testing/HopFrame.Testing/Program.cs
+++ b/testing/HopFrame.Testing/Program.cs
@@ -1,11 +1,8 @@
-using System.Collections;
-using HopFrame.Core.Events;
using HopFrame.Testing;
using Microsoft.FluentUI.AspNetCore.Components;
using HopFrame.Testing.Components;
using HopFrame.Testing.Models;
using HopFrame.Web;
-using HopFrame.Web.Components.Pages;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
@@ -76,8 +73,13 @@ builder.Services.AddHopFrame(options => {
})*/;
context.Table()
- .SetOrderIndex(-1);
+ .SetOrderIndex(-1)
+ .Ignore(true);
});
+
+ options.AddCustomView("Counter", "/counter")
+ .SetDescription("A custom view")
+ .SetPolicy("counter.view");
});
var app = builder.Build();