diff --git a/src/AzureOpenAIProxy.PlaygroundApp/AzureOpenAIProxy.PlaygroundApp.csproj b/src/AzureOpenAIProxy.PlaygroundApp/AzureOpenAIProxy.PlaygroundApp.csproj index c9ffe317..84a2b5bf 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/AzureOpenAIProxy.PlaygroundApp.csproj +++ b/src/AzureOpenAIProxy.PlaygroundApp/AzureOpenAIProxy.PlaygroundApp.csproj @@ -10,6 +10,7 @@ + diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor index 6341ab35..f1b5b32f 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminNewEvent.razor @@ -3,3 +3,5 @@ New event

New event

+ + \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/NewEventDetailsComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/NewEventDetailsComponent.razor new file mode 100644 index 00000000..87d9e6e7 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/NewEventDetailsComponent.razor @@ -0,0 +1,181 @@ +@using AzureOpenAIProxy.PlaygroundApp.Clients +@using AzureOpenAIProxy.PlaygroundApp.Models; + +@using System.Globalization + +@using NodaTime +@using NodaTime.Extensions +@using NodaTime.TimeZones + + + @if (adminEventDetails == null) + { +

Loading...

+ } + else + { + New Event + +
+

Event Infomation

+ + + Title + + + + + Summary + + + + + Description + + + + + Event Start Date + + + + + + Event End Date + + + + + + Time Zone + + @foreach (var timeZone in timeZoneList) + { + @timeZone.Id + } + + +
+ +
+

Event Organizer

+ + + Organizer Name + + + + + + Organizer Email + + +
+ +
+

Event Coorganizers

+ + + Coorgnizer Name + + + + + Coorgnizer Email + + +
+ +
+

Event Configuration

+ + + Max Token Cap + + + + + Daily Request Cap + + +
+ +
+ Add Event + Cancel +
+
+ } +
+ + +@code { + private List? timeZoneList; + private AdminEventDetails? adminEventDetails; + private DateTimeOffset currentTime = DateTimeOffset.UtcNow; + + [Parameter] + public string? Id { get; set; } + + protected override async Task OnInitializedAsync() + { + adminEventDetails = adminEventDetails == null ? new() : adminEventDetails; + + timeZoneList = DateTimeZoneProviders.Tzdb.GetAllZones().ToList(); + + CultureInfo customCulture = (CultureInfo)CultureInfo.CurrentCulture.Clone(); + customCulture.DateTimeFormat.ShortDatePattern = "yyyy-MM-dd"; + customCulture.DateTimeFormat.ShortTimePattern = "HH:mm"; + + CultureInfo.DefaultThreadCurrentCulture = customCulture; + CultureInfo.DefaultThreadCurrentUICulture = customCulture; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var timezoneId = GetIanaTimezoneId(); + currentTime = GetCurrentDateTimeOffset(timezoneId); + + adminEventDetails.DateStart = currentTime.AddHours(1).AddMinutes(-currentTime.Minute); + adminEventDetails.DateEnd = currentTime.AddDays(1).AddHours(1).AddMinutes(-currentTime.Minute); + adminEventDetails.TimeZone = timezoneId; + + await InvokeAsync(StateHasChanged); + } + } + + private async Task AddEvent() + { + await Task.CompletedTask; + } + + private async Task CancelEvent() + { + await Task.CompletedTask; + } + + private string GetIanaTimezoneId() + { + string timezoneId = TimeZoneInfo.Local.Id; + + if (OperatingSystem.IsWindows()) + { + if (TimeZoneInfo.TryConvertWindowsIdToIanaId(timezoneId, out var ianaTimezoneId)) + { + timezoneId = ianaTimezoneId; + } + } + + return timezoneId; + } + + private DateTimeOffset GetCurrentDateTimeOffset(string timezoneId) + { + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timezoneId); + + return TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, timeZoneInfo); + } +} + diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/NewEventDetailsComponent.razor.css b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/NewEventDetailsComponent.razor.css new file mode 100644 index 00000000..b6975111 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/NewEventDetailsComponent.razor.css @@ -0,0 +1,25 @@ +section { + margin-bottom: 100px +} + +::deep .create-input-label { + width: 200px; + --type-ramp-base-font-size: 22px; +} + +::deep .create-fluent-stack { + height: 100px; +} + +.button-section { + display: flex; + justify-content: center; + gap: 50px; +} + +.button { + width: 150px; + height: 50px; + font-size: 16px; + margin: 0 10px; +} diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Models/AdminEventDetails.cs b/src/AzureOpenAIProxy.PlaygroundApp/Models/AdminEventDetails.cs index 0221a475..7783fecb 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Models/AdminEventDetails.cs +++ b/src/AzureOpenAIProxy.PlaygroundApp/Models/AdminEventDetails.cs @@ -1,60 +1,60 @@ -using System.Text.Json.Serialization; - -namespace AzureOpenAIProxy.PlaygroundApp.Models; - -/// -/// This represent the event detail data for response by admin event endpoint. -/// -public class AdminEventDetails : EventDetails -{ - /// - /// Gets or sets the event description. - /// - public string? Description { get; set; } - - /// - /// Gets or sets the event start date. - /// - [JsonRequired] - public DateTimeOffset DateStart { get; set; } - - /// - /// Gets or sets the event end date. - /// - [JsonRequired] - public DateTimeOffset DateEnd { get; set; } - - /// - /// Gets or sets the event start to end date timezone. - /// - [JsonRequired] - public string TimeZone { get; set; } = string.Empty; - - /// - /// Gets or sets the event active status. - /// - [JsonRequired] - public bool IsActive { get; set; } - - /// - /// Gets or sets the event organizer name. - /// - [JsonRequired] - public string OrganizerName { get; set; } = string.Empty; - - /// - /// Gets or sets the event organizer email. - /// - [JsonRequired] - public string OrganizerEmail { get; set; } = string.Empty; - - /// - /// Gets or sets the event coorganizer name. - /// - public string? CoorganizerName { get; set; } - - /// - /// Gets or sets the event coorganizer email. - /// - public string? CoorganizerEmail { get; set; } +using System.Text.Json.Serialization; + +namespace AzureOpenAIProxy.PlaygroundApp.Models; + +/// +/// This represent the event detail data for response by admin event endpoint. +/// +public class AdminEventDetails : EventDetails +{ + /// + /// Gets or sets the event description. + /// + public string? Description { get; set; } + + /// + /// Gets or sets the event start date. + /// + [JsonRequired] + public DateTimeOffset DateStart { get; set; } + + /// + /// Gets or sets the event end date. + /// + [JsonRequired] + public DateTimeOffset DateEnd { get; set; } + + /// + /// Gets or sets the event start to end date timezone. + /// + [JsonRequired] + public string TimeZone { get; set; } = string.Empty; + + /// + /// Gets or sets the event active status. + /// + [JsonRequired] + public bool IsActive { get; set; } + + /// + /// Gets or sets the event organizer name. + /// + [JsonRequired] + public string OrganizerName { get; set; } = string.Empty; + + /// + /// Gets or sets the event organizer email. + /// + [JsonRequired] + public string OrganizerEmail { get; set; } = string.Empty; + + /// + /// Gets or sets the event coorganizer name. + /// + public string? CoorganizerName { get; set; } + + /// + /// Gets or sets the event coorganizer email. + /// + public string? CoorganizerEmail { get; set; } } \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Models/EventDetails.cs b/src/AzureOpenAIProxy.PlaygroundApp/Models/EventDetails.cs index 56cc3a7c..bade312c 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Models/EventDetails.cs +++ b/src/AzureOpenAIProxy.PlaygroundApp/Models/EventDetails.cs @@ -1,39 +1,39 @@ -using System.Text.Json.Serialization; - -namespace AzureOpenAIProxy.PlaygroundApp.Models; - -/// -/// This represents the event's detailed data for response by EventEndpoint. -/// -public class EventDetails -{ - /// - /// Gets or sets the event id. - /// - [JsonRequired] - public Guid EventId { get; set; } - - /// - /// Gets or sets the event title name. - /// - [JsonRequired] - public string Title { get; set; } = string.Empty; - - /// - /// Gets or sets the event summary. - /// - [JsonRequired] - public string Summary { get; set; } = string.Empty; - - /// - /// Gets or sets the Azure OpenAI Service request max token capacity. - /// - [JsonRequired] - public int MaxTokenCap { get; set; } - - /// - /// Gets or sets the Azure OpenAI Service daily request capacity. - /// - [JsonRequired] - public int DailyRequestCap { get; set; } +using System.Text.Json.Serialization; + +namespace AzureOpenAIProxy.PlaygroundApp.Models; + +/// +/// This represents the event's detailed data for response by EventEndpoint. +/// +public class EventDetails +{ + /// + /// Gets or sets the event id. + /// + [JsonRequired] + public Guid EventId { get; set; } + + /// + /// Gets or sets the event title name. + /// + [JsonRequired] + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the event summary. + /// + [JsonRequired] + public string Summary { get; set; } = string.Empty; + + /// + /// Gets or sets the Azure OpenAI Service request max token capacity. + /// + [JsonRequired] + public int MaxTokenCap { get; set; } + + /// + /// Gets or sets the Azure OpenAI Service daily request capacity. + /// + [JsonRequired] + public int DailyRequestCap { get; set; } } \ No newline at end of file diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/NewEventDetailsPageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/NewEventDetailsPageTests.cs new file mode 100644 index 00000000..5ce13d5d --- /dev/null +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/NewEventDetailsPageTests.cs @@ -0,0 +1,68 @@ +using System.Net; + +using AzureOpenAIProxy.AppHost.Tests.Fixtures; + +using FluentAssertions; + +namespace AzureOpenAIProxy.AppHost.Tests.PlaygroundApp.Pages; +public class NewEventDetailsPageTests(AspireAppHostFixture host) : IClassFixture +{ + [Fact] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_OK() + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var response = await httpClient.GetAsync("/admin/events/new"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Theory] + [InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_CSS_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/admin/events/new"); + + // Assert + html.Should().Contain(expected); + } + + [Theory] + [InlineData("_content/Microsoft.FluentUI.AspNetCore.Components/Microsoft.FluentUI.AspNetCore.Components.lib.module.js")] + public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_JavaScript_Elements(string expected) + { + // Arrange + using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // Act + var html = await httpClient.GetStringAsync("/admin/events/new"); + + // Assert + html.Should().Contain(expected); + } + + //[Theory] + //[InlineData("
")] + //public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected) + //{ + // // Arrange + // using var httpClient = host.App!.CreateHttpClient("playgroundapp"); + // await host.ResourceNotificationService.WaitForResourceAsync("playgroundapp", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); + + // // Act + // var html = await httpClient.GetStringAsync("/admin/events/new"); + + // // Assert + // html.Should().Contain(expected); + //} +} diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/NewEventDetailsPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/NewEventDetailsPageTests.cs new file mode 100644 index 00000000..8731f3a9 --- /dev/null +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/NewEventDetailsPageTests.cs @@ -0,0 +1,162 @@ +using FluentAssertions; + +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] + +public class NewEventDetailsPageTests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() + { + IgnoreHTTPSErrors = true, + }; + + [SetUp] + public async Task Init() + { + await Page.GotoAsync("https://localhost:5001/admin/events/new"); + await Page.WaitForLoadStateAsync(LoadState.NetworkIdle); + } + + [Test] + [TestCase("event-title")] + [TestCase("event-summary")] + [TestCase("event-description")] + [TestCase("event-start-date")] + [TestCase("event-start-time")] + [TestCase("event-end-date")] + [TestCase("event-end-time")] + [TestCase("event-timezone")] + [TestCase("event-organizer-name")] + [TestCase("event-organizer-email")] + [TestCase("event-coorgnizer-name")] + [TestCase("event-coorgnizer-email")] + [TestCase("event-max-token-cap")] + [TestCase("event-daily-request-cap")] + [TestCase("admin-event-detail-add")] + [TestCase("admin-event-detail-cancel")] + public async Task Given_New_Event_Details_Page_When_Navigated_Then_It_Should_Load_Correctly(string id) + { + // Act + var element = Page.Locator($"#{id}"); + + // Assert + await Expect(element).ToBeVisibleAsync(); + } + + [Test] + public async Task Given_Input_Event_Timezone_When_Initialized_Timezone_Then_It_Should_Update_Value() + { + // Arrange + var inputTimezone = Page.Locator("#event-timezone"); + + string timeZone = GetIanaTimezoneId(); + + // Act + string inputTimezoneValue = await inputTimezone.GetAttributeAsync("current-value"); + + // Assert + inputTimezoneValue.Should().Be(timeZone); + } + + [Test] + public async Task Given_Input_Event_Start_Date_When_Initialized_Timezone_Then_It_Should_Update_Value() + { + // Arrange + var inputStartDate = Page.Locator("#event-start-date"); + + string timezoneId = GetIanaTimezoneId(); + DateTimeOffset currentTime = GetCurrentDateTimeOffset(timezoneId); + var startTime = currentTime.AddHours(1).AddMinutes(-currentTime.Minute); + + // Act + var inputStartDateValue = await inputStartDate.GetAttributeAsync("current-value"); + + // Assert + inputStartDateValue.Should().Be(startTime.ToString("yyyy-MM-dd")); + } + + [Test] + public async Task Given_Input_Event_Start_Time_When_Initialized_Timezone_Then_It_Should_Update_Value() + { + // Arrange + var inputStartTime = Page.Locator("#event-start-time"); + + string timezoneId = GetIanaTimezoneId(); + DateTimeOffset currentTime = GetCurrentDateTimeOffset(timezoneId); + var startTime = currentTime.AddHours(1).AddMinutes(-currentTime.Minute); + + // Act + var inputStartTimeValue = await inputStartTime.GetAttributeAsync("current-value"); + + // Assert + inputStartTimeValue.Should().Be(startTime.ToString("HH:mm")); + } + + [Test] + public async Task Given_Input_Event_End_Date_When_Initialized_Timezone_Then_It_Should_Update_Value() + { + // Arrange + var inputEndDate = Page.Locator("#event-end-date"); + + string timezoneId = GetIanaTimezoneId(); + DateTimeOffset currentTime = GetCurrentDateTimeOffset(timezoneId); + var endTime = currentTime.AddDays(1).AddHours(1).AddMinutes(-currentTime.Minute); + + // Act + var inputEndDateValue = await inputEndDate.GetAttributeAsync("current-value"); + + // Assert + inputEndDateValue.Should().Be(endTime.ToString("yyyy-MM-dd")); + } + + [Test] + public async Task Given_Input_Event_End_Time_When_Initialized_Timezone_Then_It_Should_Update_Value() + { + // Arrange + var inputEndTime = Page.Locator("#event-end-time"); + + string timezoneId = GetIanaTimezoneId(); + DateTimeOffset currentTime = GetCurrentDateTimeOffset(timezoneId); + var endTime = currentTime.AddDays(1).AddHours(1).AddMinutes(-currentTime.Minute); + + // Act + var inputEndTimeValue = await inputEndTime.GetAttributeAsync("current-value"); + + // Assert + inputEndTimeValue.Should().Be(endTime.ToString("HH:mm")); + } + + [TearDown] + public async Task CleanUp() + { + await Page.CloseAsync(); + } + + private string GetIanaTimezoneId() + { + string timezoneId = TimeZoneInfo.Local.Id; + + if (OperatingSystem.IsWindows()) + { + if (TimeZoneInfo.TryConvertWindowsIdToIanaId(timezoneId, out var ianaTimezoneId)) + { + timezoneId = ianaTimezoneId; + } + } + + return timezoneId; + } + + private DateTimeOffset GetCurrentDateTimeOffset(string timezoneId) + { + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timezoneId); + + return TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, timeZoneInfo); + } +}