Merge branch 'UI-cleanup' into 'master'

added dynamic components, cleaned up UI

See merge request murphg62/2023-ca400-murphg62-byrnm257!15
This commit is contained in:
Malachy Byrne 2023-05-06 15:58:21 +00:00
commit 5308961541
25 changed files with 445 additions and 162 deletions

View File

@ -1,13 +1,25 @@
public class Component {
public int Type {get; set;}
public string Text {get; set;}
public Stats[] Stats {get; set;}
using System.Collections.Generic;
public class TableComponent {
public int Id {get; set;}
public List<Dictionary<string, object>> Data { get; set; }
public TableComponent(int id, List<Dictionary<string, object>> data){
Id = id;
Data = data;
}
}
public Component(int type, string text, Stats[] stats){
Type = type;
Text = text;
public class GraphComponent {
public int Id {get; set;}
public string Graph {get; set;}
public string Title {get; set;}
public List<Stats> Stats { get; set; }
public GraphComponent(int id, string graph, string title, List<Stats> stats){
Id = id;
Graph = graph;
Title = title;
Stats = stats;
}
}

View File

@ -1,3 +1,4 @@
public class Schema {
public Component[] components {get; set;}
public TableComponent[] tables {get; set;}
public GraphComponent[] graphs {get; set;}
}

View File

@ -0,0 +1,68 @@
@using ApexCharts
@using System.Collections.Generic
@using PanoptesFrontend.Data;
@using System.Timers
@implements IDisposable
@inject IHttpService httpService
@using PanoptesFrontend.Services
<ApexChart @ref=chart TItem="Stats" Title="@ChartTitle">
<ApexPointSeries TItem="Stats"
Items="@StatsList"
SeriesType="@SeriesTypes[@SelectedChartType]"
Name="Gross Value"
XValue="@(e => e.Label)"
YValue="@(e => e.Value)"
OrderByDescending="e=>e.Y" />
</ApexChart>
@code {
private ApexChart<Stats> chart;
private bool timerInitialized;
private Timer timer;
private Dictionary<string, SeriesType> SeriesTypes = new Dictionary<string, SeriesType>
{
{"line", SeriesType.Line},
{"area", SeriesType.Area},
{"bar", SeriesType.Bar},
{"pie", SeriesType.Pie},
{"donut", SeriesType.Donut},
{"radial-bar", SeriesType.RadialBar},
};
[Parameter]
public string ChartTitle {get; set;}
[Parameter]
public List<Stats> StatsList { get; set; }
[Parameter]
public string SelectedChartType {get; set;}
[Parameter]
public int ChartId {get; set;}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender && !timerInitialized)
{
timerInitialized = true;
timer = new Timer(5000);
timer.Elapsed += async delegate { await UpdateChartSeries(); };
timer.Enabled = true;
}
}
private async Task UpdateChartSeries()
{
string module = "localhost:8080";
var response = await httpService.GetAsync<GraphComponent>($"http://localhost:10000/{module}/{ChartId}");
StatsList = response.Stats;
await chart.UpdateSeriesAsync(true);
await InvokeAsync(() => StateHasChanged());
}
public void Dispose()
{
timer?.Stop();
timer = null;
}
}

View File

@ -0,0 +1,77 @@
@using System.Linq.Dynamic.Core
@inject IHttpService httpService
@using PanoptesFrontend.Services
<RadzenDataGrid @bind-Value=@selectedItems Data="@data" TItem="IDictionary<string, object>" ColumnWidth="200px"
AllowFiltering="true" FilterPopupRenderMode="PopupRenderMode.OnDemand" FilterMode="FilterMode.Advanced" AllowPaging="true" AllowSorting="true">
<Columns>
@foreach(var column in columns)
{
<RadzenDataGridColumn TItem="IDictionary<string, object>" Title="@column.Key" Type="column.Value"
Property="@GetColumnPropertyExpression(column.Key, column.Value)" >
<Template>
@context[@column.Key]
</Template>
</RadzenDataGridColumn>
}
</Columns>
</RadzenDataGrid>
<button @onclick="Update">Update Table</button>
@code {
IList<IDictionary<string, object>> selectedItems;
[Parameter]
public List<Dictionary<string, object>> data { get; set; }
[Parameter]
public int TableId {get; set;}
public IDictionary<string, Type> columns { get; set; }
public string GetColumnPropertyExpression(string name, Type type)
{
var expression = $@"it[""{name}""].ToString()";
if (type == typeof(int))
{
return $"int.Parse({expression})";
}
else if (type == typeof(DateTime))
{
return $"DateTime.Parse({expression})";
}
return expression;
}
protected override async Task OnInitializedAsync()
{
// Generate the columns dynamically based on the properties of the first object in the list
var firstItem = data.FirstOrDefault();
if (firstItem != null)
{
columns = firstItem.ToDictionary(p => p.Key, p => p.Value.GetType());
foreach (var column in columns.ToList())
{
if (column.Value != typeof(int) && column.Value != typeof(DateTime))
{
columns[column.Key] = typeof(string);
}
}
}
}
private async Task Update()
{
string module = "localhost:8080";
var response = await httpService.GetAsync<TableComponent>($"http://localhost:10000/{module}/{TableId}");
if (response.Data != null)
{
data.Clear();
data.AddRange(response.Data);
}
await InvokeAsync(() => StateHasChanged());
}
}

View File

@ -0,0 +1,16 @@
@using PanoptesFrontend.Data.Account;
@using PanoptesFrontend.Services;
@inject IAccountService AccountService
<button @onclick="LogoutButtonClick">Logout</button>
@code {
private string token;
private async Task LogoutButtonClick()
{
token = await AccountService.GetTokenFromLocalStorage();
await AccountService.Logout(token);
}
}

View File

@ -0,0 +1,58 @@
@page "/test"
@using System.Net;
@using PanoptesFrontend.Data
@using PanoptesFrontend.Services
@using System.Text.Json;
@using Microsoft.AspNetCore.Components.Web
@inject IHttpService httpService
@inject IJSRuntime JSRuntime
@if (tables != null | graphs != null)
{
<div class="container-fluid">
<div class="row">
<div class="col-md-6">
@foreach (var component in tables)
{
<div class="card mb-3">
<div class="card-body">
<DynamicTable data="@component.Data" TableId="@component.Id"/>
</div>
</div>
}
</div>
<div class="col-md-6">
@foreach (var component in graphs)
{
<div class="card mb-3">
<div class="card-body">
<DynamicChart StatsList="@component.Stats"
SelectedChartType="@component.Graph"
ChartTitle="@component.Title"
ChartId="@component.Id"/>
</div>
</div>
}
</div>
</div>
</div>
}
@code {
private TableComponent[] tables;
private GraphComponent[] graphs;
private string module = "localhost:8080";
protected async override Task OnInitializedAsync()
{
var schema = await httpService.GetAsync<Schema>($"http://localhost:10000/{module}/schema");
tables = schema.tables;
graphs = schema.graphs;
}
}

View File

@ -0,0 +1,16 @@
@inject IHttpService httpService
@using PanoptesFrontend.Services
<RadzenTextArea @bind-Value="sqlQuery" Style="width: 100%; height: 300px" />
<button class="btn btn-primary" @onclick="ExecuteSqlQuery">Execute Query</button>
@code {
string sqlQuery;
private async Task ExecuteSqlQuery()
{
// Send the SQL query to your backend and handle the response
var response = await httpService.PostAsync($"http://localhost:10000/sqlquery", sqlQuery);
}
}

View File

@ -1,10 +0,0 @@
<div class="card">
<div class="card-body">
<p>@Data.Text</p>
</div>
</div>
@code {
[Parameter]
public Component Data { get; set; }
}

View File

@ -1,47 +0,0 @@
@page "/test"
@using System.Net;
@using PanoptesFrontend.Data
@using PanoptesFrontend.Services
@using System.Text.Json;
@inject IHttpService httpService
@if (data != null)
{
@foreach (var component in data) {
switch (component.Type) {
case 0:
<TestCard Data="@component" />
break;
case 1:
<ApexChart TItem="Stats"
Title="Order Gross Value">
<ApexPointSeries TItem="Stats"
Items=component.Stats
SeriesType="SeriesType.Donut"
Name="Gross Value"
XValue="@(e => e.Label)"
YValue="@(e => e.Value)"
OrderByDescending="e=>e.Y" />
</ApexChart>
break;
}
}
}
@code {
private Component[] data;
protected override async Task OnInitializedAsync()
{
var response = await httpService.GetAsync<Schema>("http://localhost:10000/localhost:8080/schema");
data = response.components;
}
}

View File

@ -7,5 +7,7 @@
<script src="_content/Blazor-ApexCharts/js/apex-charts.min.js"></script>
<script src="_content/Blazor-ApexCharts/js/blazor-apex-charts.js"></script>
<link rel="stylesheet" href="_content/Radzen.Blazor/css/material-base.css">
<script src="_content/Radzen.Blazor/Radzen.Blazor.js"></script>
<component type="typeof(App)" render-mode="ServerPrerendered" />

View File

@ -12,6 +12,7 @@
<PackageReference Include="Bunit" Version="1.19.14" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="Radzen.Blazor" Version="4.10.3" />
<PackageReference Include="xunit" Version="2.4.2" />
</ItemGroup>

View File

@ -9,6 +9,7 @@ var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddHttpClient();
builder.Services.AddScoped<HttpClient>();
builder.Services.AddScoped<IHttpService, HttpService>();
builder.Services.AddScoped<IAccountService, AccountService>();
builder.Services.AddBlazoredLocalStorage();

View File

@ -9,7 +9,8 @@ public interface IAccountService
{
Task Register(AddUser model);
Task Login(LoginUser model);
Task Logout(User model);
Task Logout(string token);
Task<string> GetTokenFromLocalStorage();
}
@ -38,8 +39,16 @@ public class AccountService : IAccountService
await localStorage.SetItemAsync("authToken", token);
}
public async Task Logout(User model){
await httpService.PostAsync("http://localhost:10000/user/logout", model);
public async Task Logout(string token){
var authtoken = await localStorage.GetItemAsStringAsync("authToken");
await httpService.PostAsync("http://localhost:10000/user/logout", authtoken);
await localStorage.RemoveItemAsync("authToken");
}
public async Task<string> GetTokenFromLocalStorage()
{
return await localStorage.GetItemAsStringAsync("authToken");
}
}

View File

@ -15,8 +15,9 @@ public class HttpService : IHttpService {
private readonly HttpClient httpClient;
public HttpService(HttpClient httpClient){
this.httpClient = httpClient;
public HttpService(IHttpClientFactory httpClientFactory)
{
this.httpClient = httpClientFactory.CreateClient();
}
public async Task<T> GetAsync<T>(string endpoint){

View File

@ -1,4 +1,5 @@
@inherits LayoutComponentBase
@using PanoptesFrontend.Pages
<PageTitle>PanoptesFrontend</PageTitle>
@ -9,7 +10,8 @@
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
<a href="account/login">Login</a>
<Logout/>
</div>
<article class="content px-4">

View File

@ -5,7 +5,7 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">PanoptesFrontend</a>
<a class="navbar-brand" href="">Panoptes</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
@ -14,7 +14,7 @@
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<nav class="flex-column">
@* @if (response != null)
@if (response != null)
{
@foreach (var module in response){
<div class="nav-item px-3">
@ -23,18 +23,23 @@
</NavLink>
</div>
}
} *@
}
<div class="nav-item px-3">
<NavLink class="nav-link" href=module-config Match="NavLinkMatch.All">
<span class="oi oi-cog" aria-hidden="true"></span> Module config
</NavLink>
</div>
</nav>
</div>
@code {
@* public Module[] response;
public Module[] response;
protected override async Task OnInitializedAsync()
{
response = await httpService.GetAsync<Module[]>("http://localhost:10000/modules");
} *@
}
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

View File

@ -8,4 +8,6 @@
@using Microsoft.JSInterop
@using PanoptesFrontend
@using PanoptesFrontend.Shared
@using ApexCharts;
@using ApexCharts
@using Radzen
@using Radzen.Blazor

View File

@ -9,10 +9,10 @@ public class ModuleConfigTests : TestContext
[Fact]
public void NameInputFieldIsPresent()
{
Services.AddSingleton<IHttpService, HttpService>();
// Arrange
using var ctx = new TestContext();
ctx.Services.AddSingleton<IHttpService, HttpService>();
ctx.Services.AddHttpClient();
var cut = ctx.RenderComponent<ModConfig>();
// Act
@ -25,10 +25,10 @@ public class ModuleConfigTests : TestContext
[Fact]
public void ImageInputFieldIsPresent()
{
Services.AddSingleton<IHttpService, HttpService>();
// Arrange
using var ctx = new TestContext();
ctx.Services.AddSingleton<IHttpService, HttpService>();
ctx.Services.AddHttpClient();
var cut = ctx.RenderComponent<ModConfig>();
// Act
@ -41,10 +41,10 @@ public class ModuleConfigTests : TestContext
[Fact]
public void UserInputFieldIsPresent()
{
Services.AddSingleton<IHttpService, HttpService>();
// Arrange
using var ctx = new TestContext();
ctx.Services.AddSingleton<IHttpService, HttpService>();
ctx.Services.AddHttpClient();
var cut = ctx.RenderComponent<ModConfig>();
// Act
@ -57,10 +57,10 @@ public class ModuleConfigTests : TestContext
[Fact]
public void InternalInputFieldIsPresent()
{
Services.AddSingleton<IHttpService, HttpService>();
// Arrange
using var ctx = new TestContext();
ctx.Services.AddSingleton<IHttpService, HttpService>();
ctx.Services.AddHttpClient();
var cut = ctx.RenderComponent<ModConfig>();
// Act
@ -73,10 +73,12 @@ public class ModuleConfigTests : TestContext
[Fact]
public void EnvironmentVariableFormIsPresent()
{
Services.AddSingleton<IHttpService, HttpService>();
// Arrange
using var ctx = new TestContext();
ctx.Services.AddSingleton<IHttpService, HttpService>();
ctx.Services.AddHttpClient();
var cut = ctx.RenderComponent<ModConfig>();
// Act
@ -91,10 +93,11 @@ public class ModuleConfigTests : TestContext
[Fact]
public void VolumeFormIsPresent()
{
Services.AddSingleton<IHttpService, HttpService>();
// Arrange
using var ctx = new TestContext();
ctx.Services.AddSingleton<IHttpService, HttpService>();
ctx.Services.AddHttpClient();
var cut = ctx.RenderComponent<ModConfig>();
// Act

View File

@ -1,16 +0,0 @@
<h1>Counter</h1>
<p>
Current count: @currentCount
</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
int currentCount = 0;
void IncrementCount()
{
currentCount++;
}
}

View File

@ -1,31 +0,0 @@
namespace PanoptesTest;
/// <summary>
/// These tests are written entirely in C#.
/// Learn more at https://bunit.dev/docs/getting-started/writing-tests.html#creating-basic-tests-in-cs-files
/// </summary>
public class CounterCSharpTests : TestContext
{
[Fact]
public void CounterStartsAtZero()
{
// Arrange
var cut = RenderComponent<Counter>();
// Assert that content of the paragraph shows counter at zero
cut.Find("p").MarkupMatches("<p>Current count: 0</p>");
}
[Fact]
public void ClickingButtonIncrementsCounter()
{
// Arrange
var cut = RenderComponent<Counter>();
// Act - click button to increment counter
cut.Find("button").Click();
// Assert that the counter was incremented
cut.Find("p").MarkupMatches("<p>Current count: 1</p>");
}
}

View File

@ -1,29 +0,0 @@
@inherits TestContext
These tests are written entirely in razor and C# syntax.
Learn more at https://bunit.dev/docs/getting-started/writing-tests.html#creating-basic-tests-in-razor-files
@code {
[Fact]
public void CounterStartsAtZero()
{
// Arrange
var cut = Render(@<Counter />);
// Assert that content of the paragraph shows counter at zero
cut.Find("p").MarkupMatches(@<p>Current count: 0</p>);
}
[Fact]
public void ClickingButtonIncrementsCounter()
{
// Arrange
var cut = Render(@<Counter />);
// Act - click button to increment counter
cut.Find("button").Click();
// Assert that the counter was incremented
cut.Find("p").MarkupMatches(@<p>Current count: 1</p>);
}
}

View File

@ -0,0 +1,102 @@
using PanoptesFrontend.Data;
using PanoptesFrontend.Pages;
using System.Collections.Generic;
using PanoptesFrontend.Services;
using ApexCharts;
using Microsoft.JSInterop;
public class DynamicChartTests
{
[Fact]
public void DynamicChartRendersChartWithCorrectType()
{
// Arrange
var testContext = new TestContext();
testContext.Services.AddSingleton<IHttpService, HttpService>();
testContext.Services.AddHttpClient();
testContext.JSInterop.SetupVoid("blazor_apexchart.renderChart", _ => true);
var chartTitle = "Example Chart";
var statsList = new List<Stats>
{
new Stats { Label = "A", Value = 1 },
new Stats { Label = "B", Value = 2 },
new Stats { Label = "C", Value = 3 }
};
var cut = testContext.RenderComponent<DynamicChart>(
("ChartTitle", chartTitle),
("StatsList", statsList),
("SelectedChartType", "bar")
);
// act
cut.Instance.SelectedChartType = "line";
var actualChartType = cut.Instance.SelectedChartType;
// assert
var expectedChartType = "line";
Assert.Equal(expectedChartType, actualChartType);
}
[Fact]
public void DynamicChartRendersChartWithCorrectTitle()
{
// Arrange
var testContext = new TestContext();
testContext.Services.AddSingleton<IHttpService, HttpService>();
testContext.Services.AddHttpClient();
testContext.JSInterop.SetupVoid("blazor_apexchart.renderChart", _ => true);
var chartTitle = "Example Chart";
var statsList = new List<Stats>
{
new Stats { Label = "A", Value = 1 },
new Stats { Label = "B", Value = 2 },
new Stats { Label = "C", Value = 3 }
};
var cut = testContext.RenderComponent<DynamicChart>(
("ChartTitle", chartTitle),
("StatsList", statsList),
("SelectedChartType", "line")
);
// Act
var actualTitle = cut.Instance.ChartTitle;
// Assert
Assert.Equal(chartTitle, actualTitle);
}
[Fact]
public void DynamicChartRendersWithStatsList()
{
// Arrange
var testContext = new TestContext();
testContext.Services.AddSingleton<IHttpService, HttpService>();
testContext.Services.AddHttpClient();
testContext.JSInterop.SetupVoid("blazor_apexchart.renderChart", _ => true);
var statsList = new List<Stats>
{
new Stats { Label = "Jan", Value = 100 },
new Stats { Label = "Feb", Value = 200 },
new Stats { Label = "Mar", Value = 150 }
};
var chartTitle = "My Chart";
var selectedChartType = "line";
// Act
var cut = testContext.RenderComponent<DynamicChart>(
("StatsList", statsList),
("ChartTitle", chartTitle),
("SelectedChartType", selectedChartType)
);
var chart = cut.FindComponent<ApexChart<Stats>>();
// Assert
Assert.NotNull(chart);
Assert.Equal(statsList, cut.Instance.StatsList);
}
}

View File

@ -0,0 +1,40 @@
using PanoptesFrontend.Pages;
using System.Collections.Generic;
using PanoptesFrontend.Services;
using Radzen;
using Radzen.Blazor;
public class DynamicGridTests
{
[Fact]
public void DynamicGrid_Component_Initialization()
{
// Arrange
using var ctx = new TestContext();
ctx.Services.AddSingleton<IHttpService, HttpService>();
ctx.Services.AddHttpClient();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var data = new List<Dictionary<string, object>> {
new Dictionary<string, object> {
{ "Column1", 1 },
{ "Column2", "Value" }
}
};
// Act
var cut = ctx.RenderComponent<DynamicTable>(parameters => parameters
.Add(p => p.data, data));
cut.SetParametersAndRender(parameters =>
{
parameters.Add(p => p.data, data);
});
// Assert
var grid = cut.FindComponent<RadzenDataGrid<IDictionary<string, object>>>();
Assert.NotNull(grid);
Assert.NotNull(grid.Instance);
}
}

View File

@ -20,7 +20,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
<PackageReference Include="Radzen.Blazor" Version="4.10.3" />
</ItemGroup>
<ItemGroup>