diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminUpdateEvent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminUpdateEvent.razor new file mode 100644 index 00000000..0e115dbc --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/Pages/AdminUpdateEvent.razor @@ -0,0 +1,14 @@ +@page "/admin/events/edit/{EventId:guid}" + +@using AzureOpenAIProxy.PlaygroundApp.Models; + +Update Event + +

Update Event

+ + + +@code { + [Parameter] + public Guid EventId { get; set; } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor index 09c43446..a0ff196c 100644 --- a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/AdminEventsComponent.razor @@ -1,5 +1,7 @@ @using AzureOpenAIProxy.PlaygroundApp.Models +@inject NavigationManager NavigationManager + @if (eventDetails == null) { @@ -21,7 +23,7 @@ - + @@ -89,4 +91,9 @@ private string? AriaCurrentValue(int pageIndex) => pagination.CurrentPageIndex == pageIndex ? "page" : null; + + private void NavigateToEdit(Guid eventId) + { + NavigationManager.NavigateTo($"/admin/events/edit/{eventId}"); + } } diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/UpdateEventDetailsComponent.razor b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/UpdateEventDetailsComponent.razor new file mode 100644 index 00000000..b9bff807 --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/UpdateEventDetailsComponent.razor @@ -0,0 +1,195 @@ +@using AzureOpenAIProxy.PlaygroundApp.Models +@using NodaTime +@using NodaTime.Extensions +@using System.Globalization + +@inject NavigationManager NavigationManager + + + @if(adminEventDetails == null) + { +

Loading...

+ } + else + { + Update 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 + + +
+ +
+ Update Event + Cancel +
+
+ } +
+ +@code { + [Parameter] + public string? Id { get; set; } + + [Parameter] + public Guid EventId { get; set;} + + private List? timeZoneList = []; + private AdminEventDetails? adminEventDetails; + + protected override void OnInitialized() + { + 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) + { + // TODO: GET AdminEventDetails through the /admin/events/{eventId} API. + await Task.Delay(1000); + var timezoneId = GetIanaTimezoneId(); + var currentTime = GetCurrentDateTimeOffset(timezoneId); + + // Make dummy data + adminEventDetails = new AdminEventDetails + { + EventId = EventId, + Title = "dummy title", + Summary = "dummy summary", + Description = "dummy description", + DateStart = currentTime.AddHours(1).AddMinutes(-currentTime.Minute), + DateEnd = currentTime.AddDays(1).AddHours(1).AddMinutes(-currentTime.Minute), + TimeZone = timezoneId, + IsActive = true, + OrganizerName = $"dummy organizer", + OrganizerEmail = $"dummy_user@gmail.com", + CoorganizerName = $"dummy coorganizer", + CoorganizerEmail = $"dummy_supprot@gmail.com", + MaxTokenCap = 10000, + DailyRequestCap = 1000 + }; + + await InvokeAsync(StateHasChanged); + } + } + + private async Task UpdateEvent() + { + // TODO: PUT AdminEventDetails through the /admin/events/{eventId} API. + await Task.CompletedTask; + } + + private void CancelUpdate() + { + NavigationManager.NavigateTo("/admin/events", forceLoad: false); + } + + 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); + } +} \ No newline at end of file diff --git a/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/UpdateEventDetailsComponent.razor.css b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/UpdateEventDetailsComponent.razor.css new file mode 100644 index 00000000..76e38e4d --- /dev/null +++ b/src/AzureOpenAIProxy.PlaygroundApp/Components/UI/Admin/UpdateEventDetailsComponent.razor.css @@ -0,0 +1,25 @@ +section { + margin-bottom: 100px +} + +::deep .update-input-label { + width: 200px; + --type-ramp-base-font-size: 22px; +} + +::deep .update-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/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminNewEventPageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminNewEventPageTests.cs index 2cc2a3f3..31a51ff3 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminNewEventPageTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminNewEventPageTests.cs @@ -53,7 +53,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Jav } [Theory] - [InlineData("
")] + [InlineData("
")] public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected) { // Arrange diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminPageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminPageTests.cs index b1b2f936..1a7f912d 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminPageTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminPageTests.cs @@ -53,7 +53,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Jav } [Theory] - [InlineData("
")] + [InlineData("
")] public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected) { // Arrange diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminUpdateEventTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminUpdateEventTests.cs new file mode 100644 index 00000000..f00b2195 --- /dev/null +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/AdminUpdateEventTests.cs @@ -0,0 +1,81 @@ +using System.Net; + +using AzureOpenAIProxy.AppHost.Tests.Fixtures; + +using FluentAssertions; + +namespace AzureOpenAIProxy.AppHost.Tests.PlaygroundApp.Pages; + +public class AdminUpdateEventPageTests(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)); + + var eventId = Guid.NewGuid(); + var expectedUrl = $"/admin/events/edit/{eventId}"; + + // Act + var response = await httpClient.GetAsync(expectedUrl); + + // 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)); + + var eventId = Guid.NewGuid(); + var expectedUrl = $"/admin/events/edit/{eventId}"; + + // Act + var html = await httpClient.GetStringAsync(expectedUrl); + + // 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)); + + var eventId = Guid.NewGuid(); + var expectedUrl = $"/admin/events/edit/{eventId}"; + + // Act + var html = await httpClient.GetStringAsync(expectedUrl); + + // 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)); + + var eventId = Guid.NewGuid(); + var expectedUrl = $"/admin/events/edit/{eventId}"; + + // Act + var html = await httpClient.GetStringAsync(expectedUrl); + + // Assert + html.Should().Contain(expected); + } +} \ No newline at end of file diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/EventsPageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/EventsPageTests.cs index 45377403..e25bdd7c 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/EventsPageTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/EventsPageTests.cs @@ -53,7 +53,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Jav } [Theory] - [InlineData("
")] + [InlineData("
")] public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected) { // Arrange diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/HomePageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/HomePageTests.cs index 9f276ff5..c316cf81 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/HomePageTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/HomePageTests.cs @@ -53,7 +53,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Jav } [Theory] - [InlineData("
")] + [InlineData("
")] public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected) { // Arrange diff --git a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs index bb680273..6e2bf859 100644 --- a/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs +++ b/test/AzureOpenAIProxy.AppHost.Tests/PlaygroundApp/Pages/PlaygroundPageTests.cs @@ -51,7 +51,7 @@ public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_Jav } [Theory] - [InlineData("
")] + [InlineData("
")] public async Task Given_Resource_When_Invoked_Endpoint_Then_It_Should_Return_HTML_Elements(string expected) { // Arrange diff --git a/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminUpdateEventPageTests.cs b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminUpdateEventPageTests.cs new file mode 100644 index 00000000..70e6fad2 --- /dev/null +++ b/test/AzureOpenAIProxy.PlaygroundApp.Tests/Pages/AdminUpdateEventPageTests.cs @@ -0,0 +1,159 @@ +using FluentAssertions; + +using Microsoft.Playwright; +using Microsoft.Playwright.NUnit; + +namespace AzureOpenAIProxy.PlaygroundApp.Tests.Pages; + +[Parallelizable(ParallelScope.Self)] +[TestFixture] +[Property("Category", "Integration")] +public class AdminUpdateEventPageTests : PageTest +{ + public override BrowserNewContextOptions ContextOptions() => new() { IgnoreHTTPSErrors = true, }; + + [SetUp] + public async Task Init() + { + var eventId = Guid.NewGuid(); + await Page.GotoAsync($"https://localhost:5001/admin/events/edit/{eventId}"); + 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-update")] + [TestCase("admin-event-detail-cancel")] + public async Task Given_Update_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); + } +} \ No newline at end of file