diff --git a/.github/workflows/build-push-container.yaml b/.github/workflows/build-push-container.yaml
deleted file mode 100644
index dd6f400..0000000
--- a/.github/workflows/build-push-container.yaml
+++ /dev/null
@@ -1,60 +0,0 @@
-#
-name: Create and publish a Docker image
-
-# Configures this workflow to run every time a change is pushed to the branch called `release`.
-on:
- push:
- branches: ["release"]
- workflow_dispatch:
-
-# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository }}
-
-# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
-jobs:
- build-and-push-image:
- runs-on: self-hosted
- # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
- permissions:
- contents: read
- packages: write
- attestations: write
- id-token: write
- #
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
- - name: Log in to the Container registry
- uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
- - name: Extract metadata (tags, labels) for Docker
- id: meta
- uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
- # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
- # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
- - name: Build and push Docker image
- id: push
- uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
- with:
- context: .
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
-
- # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
- - name: Generate artifact attestation
- uses: actions/attest-build-provenance@v2
- with:
- subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
- subject-digest: ${{ steps.push.outputs.digest }}
- push-to-registry: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..ec403c4
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,122 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - '**'
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ version:
+ runs-on: ubuntu-latest
+ outputs:
+ semVer: ${{ steps.gitversion.outputs.semVer }}
+ majorMinorPatch: ${{ steps.gitversion.outputs.majorMinorPatch }}
+ preReleaseTag: ${{ steps.gitversion.outputs.preReleaseTag }}
+ fullSemVer: ${{ steps.gitversion.outputs.fullSemVer }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Full history needed for GitVersion
+
+ - name: Install GitVersion
+ uses: gittools/actions/gitversion/setup@v3.1.1
+ with:
+ versionSpec: '5.x'
+
+ - name: Determine Version
+ id: gitversion
+ uses: gittools/actions/gitversion/execute@v3.1.1
+ with:
+ useConfigFile: true
+
+ - name: Display Version
+ run: |
+ echo "SemVer: ${{ steps.gitversion.outputs.semVer }}"
+ echo "FullSemVer: ${{ steps.gitversion.outputs.fullSemVer }}"
+
+ build-and-test:
+ runs-on: ubuntu-latest
+ needs: version
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+ cache-dependency-path: app/sb-explorer-ui/package-lock.json
+
+ - name: Install frontend dependencies
+ working-directory: app/sb-explorer-ui
+ run: npm ci
+
+ - name: Build frontend
+ working-directory: app/sb-explorer-ui
+ run: npm run build
+
+ - name: Restore .NET dependencies
+ run: dotnet restore -r linux-musl-x64
+
+ - name: Build .NET solution
+ run: |
+ dotnet build --no-restore --configuration Release \
+ -p:Version=${{ needs.version.outputs.majorMinorPatch }} \
+ -p:AssemblyVersion=${{ needs.version.outputs.majorMinorPatch }}.0 \
+ -p:FileVersion=${{ needs.version.outputs.majorMinorPatch }}.0 \
+ -p:InformationalVersion=${{ needs.version.outputs.semVer }}
+
+ - name: Run tests
+ run: dotnet run --project test/ServiceBusEmulatorExplorer.Tests/ServiceBusEmulatorExplorer.Tests.csproj --configuration Release
+
+ docker:
+ runs-on: ubuntu-latest
+ needs: [version, build-and-test]
+ # Only run on main branch pushes, not on tags (release.yml handles tags)
+ if: github.ref == 'refs/heads/main' && github.event_name == 'push'
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: src/ServiceBusEmulatorExplorer/Dockerfile
+ push: true
+ build-args: |
+ VERSION=${{ needs.version.outputs.semVer }}
+ ASSEMBLY_VERSION=${{ needs.version.outputs.majorMinorPatch }}.0
+ tags: |
+ ghcr.io/${{ github.repository }}:${{ needs.version.outputs.semVer }}
+ ghcr.io/${{ github.repository }}:latest
+ labels: |
+ org.opencontainers.image.version=${{ needs.version.outputs.semVer }}
+ org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..9b7e04c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,123 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+jobs:
+ version:
+ runs-on: ubuntu-latest
+ outputs:
+ semVer: ${{ steps.gitversion.outputs.semVer }}
+ majorMinorPatch: ${{ steps.gitversion.outputs.majorMinorPatch }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install GitVersion
+ uses: gittools/actions/gitversion/setup@v3.1.1
+ with:
+ versionSpec: '5.x'
+
+ - name: Determine Version
+ id: gitversion
+ uses: gittools/actions/gitversion/execute@v3.1.1
+ with:
+ useConfigFile: true
+
+ build-and-test:
+ runs-on: ubuntu-latest
+ needs: version
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '20'
+ cache: 'npm'
+ cache-dependency-path: app/sb-explorer-ui/package-lock.json
+
+ - name: Install frontend dependencies
+ working-directory: app/sb-explorer-ui
+ run: npm ci
+
+ - name: Build frontend
+ working-directory: app/sb-explorer-ui
+ run: npm run build
+
+ - name: Restore .NET dependencies
+ run: dotnet restore -r linux-musl-x64
+
+ - name: Build .NET solution
+ run: |
+ dotnet build --no-restore --configuration Release \
+ -p:Version=${{ needs.version.outputs.majorMinorPatch }} \
+ -p:AssemblyVersion=${{ needs.version.outputs.majorMinorPatch }}.0 \
+ -p:FileVersion=${{ needs.version.outputs.majorMinorPatch }}.0 \
+ -p:InformationalVersion=${{ needs.version.outputs.semVer }}
+
+ - name: Run tests
+ run: dotnet run --project test/ServiceBusEmulatorExplorer.Tests/ServiceBusEmulatorExplorer.Tests.csproj --configuration Release
+
+ release:
+ runs-on: ubuntu-latest
+ needs: [version, build-and-test]
+ permissions:
+ contents: write
+ packages: write
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: src/ServiceBusEmulatorExplorer/Dockerfile
+ push: true
+ build-args: |
+ VERSION=${{ needs.version.outputs.semVer }}
+ ASSEMBLY_VERSION=${{ needs.version.outputs.majorMinorPatch }}.0
+ tags: |
+ ghcr.io/${{ github.repository }}:${{ needs.version.outputs.semVer }}
+ ghcr.io/${{ github.repository }}:${{ needs.version.outputs.majorMinorPatch }}
+ ghcr.io/${{ github.repository }}:latest
+ labels: |
+ org.opencontainers.image.version=${{ needs.version.outputs.semVer }}
+ org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ generate_release_notes: true
+ name: v${{ needs.version.outputs.semVer }}
+ body: |
+ ## Docker Image
+
+ ```bash
+ docker pull ghcr.io/${{ github.repository }}:${{ needs.version.outputs.semVer }}
+ ```
diff --git a/.gitignore b/.gitignore
index 08f54b0..bc78471 100644
--- a/.gitignore
+++ b/.gitignore
@@ -421,7 +421,7 @@ autom4te.cache/
*.tar.gz
tarballs/
test-results/
-data/
+
# Mac bundle stuff
*.dmg
*.app
diff --git a/Components/App.razor b/Components/App.razor
deleted file mode 100644
index 09a2087..0000000
--- a/Components/App.razor
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Components/Layout/MainLayout.razor b/Components/Layout/MainLayout.razor
deleted file mode 100644
index b3dac99..0000000
--- a/Components/Layout/MainLayout.razor
+++ /dev/null
@@ -1,137 +0,0 @@
-@using Microsoft.Extensions.Caching.Distributed
-@using Microsoft.Extensions.Options
-@using SbExplorer.Mud.Helpers
-@inherits LayoutComponentBase
-
-
-
-
-
-
-
-
- Service Bus Explorer
-
-
-
- Clear cache
-
-
-
-
-
- @Body
-
-
-
-
-
- An unhandled error has occurred.
-
Reload
-
🗙
-
-
-@code {
- private bool _drawerOpen = true;
- private bool _isDarkMode = false;
- private MudTheme? _theme = null;
- [Inject] private IDistributedCache DistributedCache { get; set; } = null!;
- [Inject] IOptions EmulatorConfig { get; set; } = null!;
- [Inject] NavigationManager NavigationManager { get; set; } = null!;
- protected override void OnInitialized()
- {
- base.OnInitialized();
-
- _theme = new()
- {
- PaletteLight = _lightPalette,
- PaletteDark = _darkPalette,
- LayoutProperties = new LayoutProperties
- {
- DefaultBorderRadius = 50.ToString()
- },
- Typography = new Typography()
- {
- Default = new Default()
- {
- FontFamily = new[] { "Poppins", "Helvetica", "Arial", "sans-serif" }
- }
- }
- };
- }
-
-
- private async Task ClearCache()
- {
- foreach (var emulatorNamespace in EmulatorConfig.Value.UserConfig.Namespaces)
- {
- foreach (var emulatorNamespaceQueue in emulatorNamespace.Queues)
- {
- await DistributedCache.RemoveAsync(CacheKeyHelper.QueueKey(emulatorNamespace.Name, emulatorNamespaceQueue.Name));
- await DistributedCache.RemoveAsync(CacheKeyHelper.DeadLetterQueueKey(emulatorNamespace.Name, emulatorNamespaceQueue.Name));
- await DistributedCache.RemoveAsync(CacheKeyHelper.Queues);
- await DistributedCache.RemoveAsync(CacheKeyHelper.Topics);
- }
- }
-
- NavigationManager.NavigateTo(NavigationManager.Uri, true);
-
- }
-
- private void DrawerToggle()
- {
- _drawerOpen = !_drawerOpen;
- }
-
- private void DarkModeToggle()
- {
- _isDarkMode = !_isDarkMode;
- }
-
- private readonly PaletteLight _lightPalette = new()
- {
- Black = "#110e2d",
- AppbarText = "#424242",
- AppbarBackground = "rgba(255,255,255,0.8)",
- DrawerBackground = "#ffffff",
- GrayLight = "#e8e8e8",
- GrayLighter = "#f9f9f9",
- };
-
- private readonly PaletteDark _darkPalette = new()
- {
- Primary = "#7e6fff",
- Surface = "#1e1e2d",
- Background = "#1a1a27",
- BackgroundGray = "#151521",
- AppbarText = "#92929f",
- AppbarBackground = "rgba(26,26,39,0.8)",
- DrawerBackground = "#1a1a27",
- ActionDefault = "#74718e",
- ActionDisabled = "#9999994d",
- ActionDisabledBackground = "#605f6d4d",
- TextPrimary = "#b2b0bf",
- TextSecondary = "#92929f",
- TextDisabled = "#ffffff33",
- DrawerIcon = "#92929f",
- DrawerText = "#92929f",
- GrayLight = "#2a2833",
- GrayLighter = "#1e1e2d",
- Info = "#4a86ff",
- Success = "#3dcb6c",
- Warning = "#ffb545",
- Error = "#ff3f5f",
- LinesDefault = "#33323e",
- TableLines = "#33323e",
- Divider = "#292838",
- OverlayLight = "#1e1e2d80",
- };
-
- public string DarkLightModeButtonIcon => _isDarkMode switch
- {
- true => Icons.Material.Rounded.AutoMode,
- false => Icons.Material.Outlined.DarkMode,
- };
-}
-
-
diff --git a/Components/Layout/NavMenu.razor b/Components/Layout/NavMenu.razor
deleted file mode 100644
index e162846..0000000
--- a/Components/Layout/NavMenu.razor
+++ /dev/null
@@ -1,42 +0,0 @@
-@using Microsoft.Extensions.Options
-
- Emulator Overview
-
-
- @foreach (var queue in _queues)
- {
- @queue
- }
-
-
- @foreach (var topic in _topics)
- {
- @topic
- }
-
-
-
-
-
-@code
-
-
-
-{
- [Inject] private IOptions EmulatorConfig { get; set; } = null!;
-
- private List _queues = [];
- private List _topics = [];
- private string EmulatorNamespace { get; set; } = string.Empty;
-
- protected override void OnInitialized()
- {
-
- var emulatorNamespace = EmulatorConfig.Value.UserConfig.Namespaces.Single();
- EmulatorNamespace = emulatorNamespace.Name;
- _queues.AddRange(emulatorNamespace.Queues.Select(q => q.Name));
- _topics.AddRange(emulatorNamespace.Topics.Select(t => t.Name));
- base.OnInitialized();
- }
-
-}
diff --git a/Components/Pages/Error.razor b/Components/Pages/Error.razor
deleted file mode 100644
index 576cc2d..0000000
--- a/Components/Pages/Error.razor
+++ /dev/null
@@ -1,36 +0,0 @@
-@page "/Error"
-@using System.Diagnostics
-
-Error
-
-Error.
-An error occurred while processing your request.
-
-@if (ShowRequestId)
-{
-
- Request ID: @RequestId
-
-}
-
-Development Mode
-
- Swapping to Development environment will display more detailed information about the error that occurred.
-
-
- The Development environment shouldn't be enabled for deployed applications.
- It can result in displaying sensitive information from exceptions to end users.
- For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
- and restarting the app.
-
-
-@code{
- [CascadingParameter]
- private HttpContext? HttpContext { get; set; }
-
- private string? RequestId { get; set; }
- private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
-
- protected override void OnInitialized() =>
- RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
-}
diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor
deleted file mode 100644
index 5e648cd..0000000
--- a/Components/Pages/Home.razor
+++ /dev/null
@@ -1,80 +0,0 @@
-@page "/"
-@using SbExplorer.Mud.Services
-
-
-
-
- Queues
-
-
-
-
-
-
-
-
-
-
-
- Topics
-
-
-
-
-
-
-
-
-
-
-
- Subscriptions
-
-
-
-
-
-
-
-
-
-
-@code {
-
- [Inject] private NavigationManager NavigationManager { get; set; } = null!;
- [Inject] private ServiceBusAdmin ServiceBusAdmin { get; set; } = null!;
- [Inject] private ISnackbar Snackbar { get; set; } = null!;
- [Inject] private ILogger Logger { get; set; } = null!;
- private List Queues { get; set; } = [];
- private List Topics { get; set; } = [];
- private List Subscriptions { get; set; } = [];
-
-
- protected override async Task OnInitializedAsync()
- {
- try
- {
- var queues = ServiceBusAdmin.GetQueues();
-
- await foreach (var queue in queues)
- {
- Queues.Add(queue);
- }
-
- var topics = ServiceBusAdmin.GetTopics();
-
- await foreach (var topic in topics)
- {
- Topics.Add(topic);
- }
-
- Subscriptions.AddRange(ServiceBusAdmin.GetSubscriptions());
- }
- catch (Exception)
- {
- Logger.LogError("Error loading data");
- Snackbar.Add("Error loading data", Severity.Error);
- }
- }
-
-}
\ No newline at end of file
diff --git a/Components/Pages/Queue.razor b/Components/Pages/Queue.razor
deleted file mode 100644
index 5aa2032..0000000
--- a/Components/Pages/Queue.razor
+++ /dev/null
@@ -1,344 +0,0 @@
-@page "/{serviceBusNameSpace}/{queueName}"
-
-@using System.Text
-@using System.Text.Json
-@using System.Text.Json.Nodes
-@using Azure.Messaging.ServiceBus
-@using Microsoft.Extensions.Caching.Distributed
-@using SbExplorer.Mud.Helpers
-@using SbExplorer.Mud.Services
-@($"{ServiceBusNameSpace}/{QueueName}")
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Message Body
-
-
- @context.Item.Body
-
-
-
-
-
- Key
- Value
-
-
- @RtContext.Key
- @RtContext.Value
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Message Body
- @context.Item.Body
-
-
-
-
- Key
- Value
-
-
- @RtContext.Key
- @RtContext.Value
-
-
-
-
-
-
-
-
-
-
-
- @* *@
-
-
-
-
- Broker Properties
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Custom Properties
-
-
-
- @if (CustomProperties.Any())
- {
-
- @foreach (var kvp in CustomProperties)
- {
- @kvp.Key: @kvp.Value
- }
-
-
- }
-
-
- Add
-
-
-
- Validate
- Send
-
-
-
-
-@code {
- [Parameter] public string QueueName { get; set; } = string.Empty;
- [Parameter] public string ServiceBusNameSpace { get; set; } = string.Empty;
- [Inject] ServiceBusClient Client { get; set; } = null!;
- [Inject] private IDistributedCache Cache { get; set; } = null!;
- private string MessageText { get; set; } = null!;
- private List Messages { get; set; } = [];
- private List DeadLetterMessages { get; set; } = [];
- private readonly PeriodicTimer _timer = new(TimeSpan.FromSeconds(5));
- private StandaloneCodeEditor Editor { get; set; } = null!;
- private MudTabs Tabs { get; set; } = null!;
- private bool IsValidJson { get; set; }
- private BrokerProperties BrokerPropertiesModel { get; set; } = new();
- private Dictionary CustomProperties { get; set; } = new();
- private string NewKey { get; set; }
- private string NewValue { get; set; }
- private bool _brokerPropertiesExpanded;
- private bool _customPropertiesExpanded;
-
-
- private StandaloneEditorConstructionOptions EditorConstructionOptions(StandaloneCodeEditor editor)
- {
- return new StandaloneEditorConstructionOptions
- {
- AutomaticLayout = true,
- Language = "json",
- Value = MessageText
- };
- }
-
- protected override async Task OnParametersSetAsync()
- {
- await LoadMessages();
-
- if(Messages.Count is 0 && DeadLetterMessages.Count is 0)
- {
- Tabs.ActivatePanel(2);
- }
- }
-
- protected override async Task OnInitializedAsync()
- {
- await LoadMessages();
-
- if (DeadLetterMessages.Count is 0 || Messages.Count is 0)
- {
- if(Messages.Count is 0 && DeadLetterMessages.Count is 0)
- {
- Tabs.ActivatePanel(2);
- }
- _ = RunTimer();
- }
-
- StateHasChanged();
- }
-
-
- private async Task RunTimer()
- {
- while (await _timer.WaitForNextTickAsync())
- {
- await LoadMessages();
- if (DeadLetterMessages.Count is not 0 || Messages.Count is not 0)
- {
- await InvokeAsync(StateHasChanged);
- break;
- }
- }
- }
-
- private async Task LoadMessages()
- {
- if (await Cache.GetAsync(CacheKeyHelper.QueueKey(ServiceBusNameSpace, QueueName)) is { } activeMessageBytes)
- {
- var messages = JsonSerializer.Deserialize>(activeMessageBytes);
- Messages.Clear();
- Messages.AddRange(messages ?? []);
- }
-
- if (await Cache.GetAsync(CacheKeyHelper.DeadLetterQueueKey(ServiceBusNameSpace, QueueName)) is { } deadLetterMessageBytes)
- {
- var messages = JsonSerializer.Deserialize>(deadLetterMessageBytes);
- DeadLetterMessages.Clear();
- DeadLetterMessages.AddRange(messages ?? []);
- }
- }
-
- private async Task SendMessage(MouseEventArgs obj)
- {
- var sender = Client.CreateSender(QueueName);
- var message = new ServiceBusMessage(Encoding.UTF8.GetBytes(MessageText));
-
- if (BrokerPropertiesModel.LabelSubject is not null)
- {
- message.Subject = BrokerPropertiesModel.LabelSubject;
- }
-
- if (BrokerPropertiesModel.CorrelationId is not null)
- {
- message.CorrelationId = BrokerPropertiesModel.CorrelationId;
- }
-
- if (BrokerPropertiesModel.MessageId is not null)
- {
- message.MessageId = BrokerPropertiesModel.MessageId;
- }
-
- if (BrokerPropertiesModel.ReplyTo is not null)
- {
- message.ReplyTo = BrokerPropertiesModel.ReplyTo;
- }
-
-
- foreach (var customProperty in CustomProperties)
- {
- message.ApplicationProperties.TryAdd(customProperty.Key, customProperty.Value);
- }
-
-
- await sender.SendMessageAsync(message);
- BrokerPropertiesModel.Clear();
- CustomProperties.Clear();
- MessageText = "{}";
- IsValidJson = false;
- await Editor.SetValue(MessageText);
- _brokerPropertiesExpanded = false;
- _customPropertiesExpanded = false;
-
- Console.WriteLine("Message sent");
- }
-
- private async Task Validate()
- {
- var text = await Editor.GetValue();
- try
- {
- var json = JsonNode.Parse(text);
- if (json != null)
- {
- IsValidJson = true;
- MessageText = json.ToJsonString();
- }
- }
- catch (Exception e)
- {
- Console.WriteLine(e);
- }
- }
-
- private void OnBrokerPropertiesCollapseClick() => _brokerPropertiesExpanded = !_brokerPropertiesExpanded;
- private void OnCustomPropertiesCollapseClick() => _customPropertiesExpanded = !_customPropertiesExpanded;
-
-
- public class BrokerProperties
- {
- public string? CorrelationId { get; set; }
- public string? MessageId { get; set; }
- public string? ReplyTo { get; set; }
- public string? LabelSubject { get; set; }
-
- public void Clear()
- {
- CorrelationId = null;
- MessageId = null;
- ReplyTo = null;
- LabelSubject = null;
- }
- }
-
- private void AddTableRow()
- {
- if (!string.IsNullOrEmpty(NewKey) && !string.IsNullOrEmpty(NewValue))
- {
- CustomProperties[NewKey] = NewValue;
- NewKey = string.Empty;
- NewValue = string.Empty;
- }
- }
-
-}
-
-
\ No newline at end of file
diff --git a/Components/Routes.razor b/Components/Routes.razor
deleted file mode 100644
index f756e19..0000000
--- a/Components/Routes.razor
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/Components/_Imports.razor b/Components/_Imports.razor
deleted file mode 100644
index 98c7cd9..0000000
--- a/Components/_Imports.razor
+++ /dev/null
@@ -1,18 +0,0 @@
-@using System.Net.Http
-@using System.Net.Http.Json
-@using Microsoft.AspNetCore.Components.Forms
-@using Microsoft.AspNetCore.Components.Routing
-@using Microsoft.AspNetCore.Components.Web
-@using static Microsoft.AspNetCore.Components.Web.RenderMode
-@using Microsoft.AspNetCore.Components.Web.Virtualization
-@using Microsoft.JSInterop
-@using MudBlazor
-@using MudBlazor.Services
-@using MudBlazor.Extensions
-@using MudBlazor.Extensions.Components
-@using MudBlazor.Extensions.Components.ObjectEdit
-@using SbExplorer.Mud
-@using SbExplorer.Mud.Components
-@using BlazorMonaco
-@using BlazorMonaco.Editor
-@using BlazorMonaco.Languages
diff --git a/Config/ServiceBusConfig.cs b/Config/ServiceBusConfig.cs
deleted file mode 100644
index de2ede8..0000000
--- a/Config/ServiceBusConfig.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-namespace SbExplorer.Mud.Config;
-
-public class ServiceBusConfig
-{
- public const string SectionName = "ServiceBus";
- public string ConnectionString { get; set; } = null!;
- public int RefreshIntervalMs { get; set; }
- public string EmulatorConfigFilePath { get; set; } = null!;
-}
-
-public class RedisConfig
-{
- public const string SectionName = "Redis";
- public string ConnectionString { get; set; } = null!;
- public string InstanceName { get; set; } = null!;
-}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index 3503306..0000000
--- a/Dockerfile
+++ /dev/null
@@ -1,23 +0,0 @@
-FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
-USER $APP_UID
-WORKDIR /app
-EXPOSE 80
-EXPOSE 443
-
-FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
-ARG BUILD_CONFIGURATION=Release
-WORKDIR /src
-COPY ["SbExplorer.Mud.csproj", "./"]
-RUN dotnet restore "SbExplorer.Mud.csproj"
-COPY . .
-WORKDIR "/src/"
-RUN dotnet build "SbExplorer.Mud.csproj" -c $BUILD_CONFIGURATION -o /app/build
-
-FROM build AS publish
-ARG BUILD_CONFIGURATION=Release
-RUN dotnet publish "SbExplorer.Mud.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
-
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "SbExplorer.Mud.dll"]
diff --git a/GitVersion.yml b/GitVersion.yml
new file mode 100644
index 0000000..55e9e62
--- /dev/null
+++ b/GitVersion.yml
@@ -0,0 +1,33 @@
+mode: ContinuousDeployment
+branches:
+ main:
+ regex: ^main$
+ mode: ContinuousDeployment
+ tag: ''
+ increment: Patch
+ prevent-increment-of-merged-branch-version: true
+ track-merge-target: false
+ is-release-branch: true
+ feature:
+ regex: ^feature[/-]
+ mode: ContinuousDeployment
+ tag: alpha
+ increment: Inherit
+ pull-request:
+ regex: ^(pull|pull\-requests|pr)[/-]
+ mode: ContinuousDeployment
+ tag: PullRequest
+ increment: Inherit
+ hotfix:
+ regex: ^hotfix[/-]
+ mode: ContinuousDeployment
+ tag: beta
+ increment: Patch
+ release:
+ regex: ^release[/-]
+ mode: ContinuousDeployment
+ tag: rc
+ increment: None
+ is-release-branch: true
+ignore:
+ sha: []
diff --git a/Helpers/CacheKeyHelper.cs b/Helpers/CacheKeyHelper.cs
deleted file mode 100644
index f801a66..0000000
--- a/Helpers/CacheKeyHelper.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace SbExplorer.Mud.Helpers;
-
-public static class CacheKeyHelper
-{
- public static string QueueKey(string nameSpace, string queueName) => $"{nameSpace}/{queueName}/active";
- public static string DeadLetterQueueKey(string nameSpace, string queueName) => $"{nameSpace}/{queueName}/deadletter";
-
- public static string Queues = "queues";
- public static string Topics = "topics";
-}
\ No newline at end of file
diff --git a/Helpers/ConfigParser.cs b/Helpers/ConfigParser.cs
deleted file mode 100644
index d77b16c..0000000
--- a/Helpers/ConfigParser.cs
+++ /dev/null
@@ -1,331 +0,0 @@
-//
-//
-// To parse this JSON data, add NuGet 'System.Text.Json' then do:
-//
-// using SbExplorer.Mud;
-//
-// var welcome = Welcome.FromJson(jsonString);
-#nullable enable
-#pragma warning disable CS8618
-#pragma warning disable CS8601
-#pragma warning disable CS8603
-
-namespace SbExplorer.Mud
-{
- using System;
- using System.Collections.Generic;
-
- using System.Text.Json;
- using System.Text.Json.Serialization;
- using System.Globalization;
-
- public partial class EmulatorConfig
- {
- [JsonPropertyName("UserConfig")]
- public UserConfig UserConfig { get; set; }
- }
-
- public partial class UserConfig
- {
- [JsonPropertyName("Namespaces")]
- public List Namespaces { get; set; }
-
- [JsonPropertyName("Logging")]
- public Logging Logging { get; set; }
- }
-
- public partial class Logging
- {
- [JsonPropertyName("Type")]
- public string Type { get; set; }
- }
-
- public partial class Namespace
- {
- [JsonPropertyName("Name")]
- public string Name { get; set; }
-
- [JsonPropertyName("Queues")]
- public List Queues { get; set; }
-
- [JsonPropertyName("Topics")]
- public List Topics { get; set; }
- }
-
- public partial class Queue
- {
- [JsonPropertyName("Name")]
- public string Name { get; set; }
-
- [JsonPropertyName("Properties")]
- public QueueProperties Properties { get; set; }
- }
-
- public partial class QueueProperties
- {
- [JsonPropertyName("DeadLetteringOnMessageExpiration")]
- public bool DeadLetteringOnMessageExpiration { get; set; }
-
- [JsonPropertyName("DefaultMessageTimeToLive")]
- public string DefaultMessageTimeToLive { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("DuplicateDetectionHistoryTimeWindow")]
- public string DuplicateDetectionHistoryTimeWindow { get; set; }
-
- [JsonPropertyName("ForwardDeadLetteredMessagesTo")]
- public string ForwardDeadLetteredMessagesTo { get; set; }
-
- [JsonPropertyName("ForwardTo")]
- public string ForwardTo { get; set; }
-
- [JsonPropertyName("LockDuration")]
- public string LockDuration { get; set; }
-
- [JsonPropertyName("MaxDeliveryCount")]
- public long MaxDeliveryCount { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("RequiresDuplicateDetection")]
- public bool? RequiresDuplicateDetection { get; set; }
-
- [JsonPropertyName("RequiresSession")]
- public bool RequiresSession { get; set; }
- }
-
- public partial class Topic
- {
- [JsonPropertyName("Name")]
- public string Name { get; set; }
-
- [JsonPropertyName("Properties")]
- public TopicProperties Properties { get; set; }
-
- [JsonPropertyName("Subscriptions")]
- public List Subscriptions { get; set; }
- }
-
- public partial class TopicProperties
- {
- [JsonPropertyName("DefaultMessageTimeToLive")]
- public string DefaultMessageTimeToLive { get; set; }
-
- [JsonPropertyName("DuplicateDetectionHistoryTimeWindow")]
- public string DuplicateDetectionHistoryTimeWindow { get; set; }
-
- [JsonPropertyName("RequiresDuplicateDetection")]
- public bool RequiresDuplicateDetection { get; set; }
- }
-
- public partial class Subscription
- {
- [JsonPropertyName("Name")]
- public string Name { get; set; }
-
- [JsonPropertyName("Properties")]
- public QueueProperties Properties { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("Rules")]
- public List Rules { get; set; }
- }
-
- public partial class Rule
- {
- [JsonPropertyName("Name")]
- public string Name { get; set; }
-
- [JsonPropertyName("Properties")]
- public RuleProperties Properties { get; set; }
- }
-
- public partial class RuleProperties
- {
- [JsonPropertyName("FilterType")]
- public string FilterType { get; set; }
-
- [JsonPropertyName("CorrelationFilter")]
- public CorrelationFilter CorrelationFilter { get; set; }
- }
-
- public partial class CorrelationFilter
- {
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("ContentType")]
- public string ContentType { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("CorrelationId")]
- public string CorrelationId { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("Label")]
- public string Label { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("MessageId")]
- public string MessageId { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("ReplyTo")]
- public string ReplyTo { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("ReplyToSessionId")]
- public string ReplyToSessionId { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("SessionId")]
- public string SessionId { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("To")]
- public string To { get; set; }
-
- [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
- [JsonPropertyName("Properties")]
- public CorrelationFilterProperties Properties { get; set; }
- }
-
- public partial class CorrelationFilterProperties
- {
- [JsonPropertyName("prop3")]
- public string Prop3 { get; set; }
- }
-
- public partial class EmulatorConfig
- {
- public static EmulatorConfig FromJson(string json) => JsonSerializer.Deserialize(json, SbExplorer.Mud.Converter.Settings);
- }
-
- public static class Serialize
- {
- public static string ToJson(this EmulatorConfig self) => JsonSerializer.Serialize(self, SbExplorer.Mud.Converter.Settings);
- }
-
- internal static class Converter
- {
- public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General)
- {
- Converters =
- {
- new DateOnlyConverter(),
- new TimeOnlyConverter(),
- IsoDateTimeOffsetConverter.Singleton
- },
- };
- }
-
- public class DateOnlyConverter : JsonConverter
- {
- private readonly string serializationFormat;
- public DateOnlyConverter() : this(null) { }
-
- public DateOnlyConverter(string? serializationFormat)
- {
- this.serializationFormat = serializationFormat ?? "yyyy-MM-dd";
- }
-
- public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var value = reader.GetString();
- return DateOnly.Parse(value!);
- }
-
- public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options)
- => writer.WriteStringValue(value.ToString(serializationFormat));
- }
-
- public class TimeOnlyConverter : JsonConverter
- {
- private readonly string serializationFormat;
-
- public TimeOnlyConverter() : this(null) { }
-
- public TimeOnlyConverter(string? serializationFormat)
- {
- this.serializationFormat = serializationFormat ?? "HH:mm:ss.fff";
- }
-
- public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- var value = reader.GetString();
- return TimeOnly.Parse(value!);
- }
-
- public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
- => writer.WriteStringValue(value.ToString(serializationFormat));
- }
-
- internal class IsoDateTimeOffsetConverter : JsonConverter
- {
- public override bool CanConvert(Type t) => t == typeof(DateTimeOffset);
-
- private const string DefaultDateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK";
-
- private DateTimeStyles _dateTimeStyles = DateTimeStyles.RoundtripKind;
- private string? _dateTimeFormat;
- private CultureInfo? _culture;
-
- public DateTimeStyles DateTimeStyles
- {
- get => _dateTimeStyles;
- set => _dateTimeStyles = value;
- }
-
- public string? DateTimeFormat
- {
- get => _dateTimeFormat ?? string.Empty;
- set => _dateTimeFormat = (string.IsNullOrEmpty(value)) ? null : value;
- }
-
- public CultureInfo Culture
- {
- get => _culture ?? CultureInfo.CurrentCulture;
- set => _culture = value;
- }
-
- public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
- {
- string text;
-
-
- if ((_dateTimeStyles & DateTimeStyles.AdjustToUniversal) == DateTimeStyles.AdjustToUniversal
- || (_dateTimeStyles & DateTimeStyles.AssumeUniversal) == DateTimeStyles.AssumeUniversal)
- {
- value = value.ToUniversalTime();
- }
-
- text = value.ToString(_dateTimeFormat ?? DefaultDateTimeFormat, Culture);
-
- writer.WriteStringValue(text);
- }
-
- public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
- {
- string? dateText = reader.GetString();
-
- if (string.IsNullOrEmpty(dateText) == false)
- {
- if (!string.IsNullOrEmpty(_dateTimeFormat))
- {
- return DateTimeOffset.ParseExact(dateText, _dateTimeFormat, Culture, _dateTimeStyles);
- }
- else
- {
- return DateTimeOffset.Parse(dateText, Culture, _dateTimeStyles);
- }
- }
- else
- {
- return default(DateTimeOffset);
- }
- }
-
-
- public static readonly IsoDateTimeOffsetConverter Singleton = new IsoDateTimeOffsetConverter();
- }
-}
-#pragma warning restore CS8618
-#pragma warning restore CS8601
-#pragma warning restore CS8603
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 21c0678..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2024 Tom Biddulph
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/Program.cs b/Program.cs
deleted file mode 100644
index 8912a34..0000000
--- a/Program.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-using Azure.Messaging.ServiceBus;
-using Microsoft.Extensions.Azure;
-using Microsoft.Extensions.Options;
-using MudBlazor.Extensions;
-using MudBlazor.Services;
-using SbExplorer.Mud;
-using SbExplorer.Mud.Components;
-using SbExplorer.Mud.Config;
-using SbExplorer.Mud.Services;
-
-var builder = WebApplication.CreateBuilder(args);
-// const string connectionString =
-// "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;";
-//
-//
-// const string configPath = "service-bus-config.json";
-
-
-var redisConfig = new RedisConfig();
-builder.Configuration.GetSection(RedisConfig.SectionName).Bind(redisConfig);
-
-var serviceBusConfig = new ServiceBusConfig();
-builder.Configuration.GetSection(ServiceBusConfig.SectionName).Bind(serviceBusConfig);
-builder.Services.AddSingleton(Options.Create(serviceBusConfig));
-
-if (!File.Exists(serviceBusConfig.EmulatorConfigFilePath))
-{
- throw new FileNotFoundException("Service bus config file not found", serviceBusConfig.EmulatorConfigFilePath);
-}
-
-builder.Logging.AddConsole();
-builder.Services.AddOptions();
-builder.Services.AddMemoryCache();
-
-
-builder.Services.AddStackExchangeRedisCache(opts =>
-{
- opts.Configuration = redisConfig.ConnectionString;
- opts.InstanceName = redisConfig.InstanceName;
-});
-
-builder.Services.AddHostedService();
-builder.Services.AddSingleton();
-
-try
-{
- builder.Services.AddSingleton(Options.Create(EmulatorConfig.FromJson(File.ReadAllText(serviceBusConfig.EmulatorConfigFilePath))));
-}
-catch (Exception e)
-{
- Console.Error.WriteLine("Error reading config file");
- Console.Error.WriteLine(e);
- throw;
-}
-
-builder.Services.AddMudServices();
-builder.Services.AddMudExtensions();
-builder.Services.AddAzureClients(opts =>
-{
- opts.AddClient((options, _, _) =>
- {
- return new CachingServiceBusClient(serviceBusConfig.ConnectionString, options);
- });
-});
-// Add services to the container.
-builder.Services.AddRazorComponents()
- .AddInteractiveServerComponents();
-
-var app = builder.Build();
-
-// Configure the HTTP request pipeline.
-if (!app.Environment.IsDevelopment())
-{
- app.UseExceptionHandler("/Error", createScopeForErrors: true);
- // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
- app.UseHsts();
-}
-
-app.UseHttpsRedirection();
-app.Use(MudExWebApp.MudExMiddleware);
-app.UseStaticFiles();
-app.UseAntiforgery();
-
-
-app.MapRazorComponents()
- .AddInteractiveServerRenderMode();
-
-app.Run();
\ No newline at end of file
diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json
deleted file mode 100644
index d2e4540..0000000
--- a/Properties/launchSettings.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "$schema": "http://json.schemastore.org/launchsettings.json",
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:12594",
- "sslPort": 44319
- }
- },
- "profiles": {
- "http": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "applicationUrl": "http://localhost:5261",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "https": {
- "commandName": "Project",
- "dotnetRunMessages": true,
- "launchBrowser": true,
- "applicationUrl": "https://localhost:7051;http://localhost:5261",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- }
- }
- }
diff --git a/README.md b/README.md
index 4b38fd3..1dd98ff 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,284 @@
# Service Bus Emulator Explorer
-This is a Blazor UI for the [service bus emulator](https://github.com/Azure/azure-service-bus-emulator-installer).
+A web-based UI for managing and exploring Azure Service Bus Emulator instances. This tool provides an intuitive interface to interact with queues, topics, subscriptions, and messages in the Azure Service Bus Emulator.
-
+
-## Features
+## 🚀 Features
-- [x] View active & deadlettered messages
-- [x] Send messages to a queue (including Broker Properties & Custom Properties)
+### Queue Management
-## Roadmap
-- [ ] View subscriptions
-- [ ] View topics
+- **List and Create Queues**: View all queues with runtime properties (active/dead-letter message counts, size)
+- **Send Messages**: Send messages to queues with custom properties and content
+- **Peek Messages**: Preview messages in queues without consuming them
+- **Delete Queues**: Remove queues from the emulator
+- **Dead Letter Queue Management**: Bulk delete dead-letter messages
-## Getting Started
+### Topic & Subscription Management
-The UI can be run locally or in a container.
+- **List and Create Topics**: Manage topics with real-time statistics
+- **Send Messages to Topics**: Publish messages to topics for distribution to subscriptions
+- **Subscription Management**: Create and manage subscriptions on topics
+- **Peek Subscription Messages**: Preview messages in subscriptions
+- **Delete Topics/Subscriptions**: Clean up resources
-### Running Locally
+### Message Operations
-#### Prerequisites
+- **Message Peeking**: View message content and properties without consuming
+- **Dead Letter Queue Support**: View and manage dead-letter messages for both queues and subscriptions
+- **Bulk Operations**: Bulk delete operations for dead-letter messages
+- **Message Editor**: In-browser editor for message content with syntax highlighting
-The .NET 9.0 SDK is required to build and run the ui.
+### Developer Experience
-1. Clone the repository
-2. Create a `.env` file in the repository root with the following content:
- ```
- ACCEPT_EULA=Y
- SQL_PASSWORD=your_password
- CONFIG_PATH=./service-bus-config.json
- ```
-3. run `docker compose -f compose-services.yaml up -d`
-4. run `dotnet run`
-5. Navigate to `https://localhost:5261`
+- **Modern React UI**: Built with React 19, Vite, and Fluent UI/Mantine components
+- **Real-time Updates**: Automatic refresh of entity statistics
+- **API Documentation**: OpenAPI/Scalar API documentation available
+- **Monaco Editor**: In-browser code editor for message content
+- **Observability**: Built-in OpenTelemetry support
-### Running in a container
+## 📋 Prerequisites
-#### Prerequisites
+- Docker and Docker Compose
+- .NET 10.0 SDK (for local development)
+- Node.js 18+ and npm/yarn (for frontend development)
-Ensure you've [configured the https certificate](https://learn.microsoft.com/en-us/aspnet/core/security/docker-compose-https?view=aspnetcore-9.0) for the ui. This may require editing the `docker-compose.yaml` file to point to the correct certificate.
+## 🛠️ Setup
-1. Clone the repository
-2. run `docker compose up -d`
-3. Navigate to `https://localhost`
+### Using Docker Compose (Recommended)
-## Notes
+1. **Clone the repository**
-- The emulator doesn't implement the Service Bus Administration apis so the UI will not be able to create topics or subscriptions. The ui is driven by `service-bus-config.json` to determine the topics and subscriptions to display.
-- Messages are periodically refreshed, but the UI does not automatically update when new messages are added. Refresh the page to see new messages.
-- The message cache can be cleared by clicking the "Clear Cache" button.
+ ```bash
+ git clone
+ cd Service-Bus-Emulator-Explorer
+ ```
-## Contributing
+2. **Set environment variables**
-Contributions are welcome! Please open an issue or PR to discuss changes.
+ Create a `.env` file in the root directory:
+
+ ```env
+ ACCEPT_EULA=Y
+ SQL_PASSWORD=YourStrongPassword123!
+ ```
+
+3. **Start the services**
+
+ ```bash
+ docker-compose up
+ ```
+
+ This will start:
+ - Service Bus Emulator on ports `5672` (AMQP) and `5300` (Admin)
+ - SQL Server (required by the emulator)
+ - Service Bus Explorer UI on port `8080`
+
+4. **Access the application**
+ - Web UI:
+ - API:
+ - API Documentation:
+
+### Local Development Setup
+
+#### Backend (.NET)
+
+1. **Navigate to the backend directory**
+
+ ```bash
+ cd src/ServiceBusEmulatorExplorer
+ ```
+
+2. **Configure connection strings**
+
+ Update `appsettings.Development.json`:
+
+ ```json
+ {
+ "ServiceBus": {
+ "ConnectionString": "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
+ "AdministrationConnectionString": "Endpoint=sb://localhost:5300;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
+ "RefreshIntervalMs": 5000
+ }
+ }
+ ```
+
+3. **Run the backend**
+
+ ```bash
+ dotnet run
+ ```
+
+ The API will be available at
+
+#### Frontend (React)
+
+1. **Navigate to the frontend directory**
+
+ ```bash
+ cd app/sb-explorer-ui
+ ```
+
+2. **Install dependencies**
+
+ ```bash
+ npm install
+ ```
+
+3. **Run the development server**
+
+ ```bash
+ npm run dev
+ ```
+
+ The UI will be available at
+
+4. **Build for production**
+
+ ```bash
+ npm run build
+ ```
+
+## 🔧 Configuration
+
+### Backend Configuration (`appsettings.json`)
+
+```json
+{
+ "ServiceBus": {
+ "ConnectionString": "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
+ "AdministrationConnectionString": "Endpoint=sb://localhost:5300;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
+ "RefreshIntervalMs": 5000,
+ "EmulatorConfigFilePath": "service-bus-config.json"
+ },
+ "Otlp": {
+ "Endpoint": "https://your-otel-endpoint",
+ "Headers": ""
+ }
+}
+```
+
+### Observability Configuration
+
+- `Otlp:Endpoint`: OTLP collector endpoint for OpenTelemetry
+- `Otlp:Headers`: Additional headers for OTLP exporter
+- `Otlp:EnableConsoleExporter`: Enable console exporter for debugging
+
+### Frontend Configuration
+
+Environment variables for the frontend (via `.env` or build scripts):
+
+- `VITE_API_BASE_URL`: Backend API base URL (default: `/api`)
+- `VITE_USE_MOCK`: Enable/disable mock data (for development)
+
+## 📚 API Endpoints
+
+### Queues
+
+- `GET /api/queues` - List all queues
+- `POST /api/queues` - Create a new queue
+- `DELETE /api/queues/{name}` - Delete a queue
+- `GET /api/queues/{name}/messages` - Peek messages in a queue
+- `POST /api/queues/{name}/messages` - Send message to a queue
+
+### Topics
+
+- `GET /api/topics` - List all topics
+- `POST /api/topics` - Create a new topic
+- `DELETE /api/topics/{name}` - Delete a topic
+- `POST /api/topics/{topic}/messages` - Send message to a topic
+
+### Subscriptions
+
+- `GET /api/topics/{topic}/subscriptions` - List subscriptions
+- `POST /api/topics/{topic}/subscriptions` - Create a subscription
+- `DELETE /api/topics/{topic}/subscriptions/{sub}` - Delete a subscription
+- `GET /api/topics/{topic}/subscriptions/{sub}/messages` - Peek subscription messages
+
+### Dead Letter
+
+- `POST /api/deadletter/queue/{name}/delete` - Bulk delete queue DLQ messages
+- `POST /api/deadletter/subscription/{topic}/{sub}/delete` - Bulk delete subscription DLQ messages
+
+## 🧪 Testing
+
+Run the test suite:
+
+```bash
+cd test/ServiceBusEmulatorExplorer.Tests
+dotnet test
+```
+
+## 🏗️ Project Structure
+
+```
+Service-Bus-Emulator-Explorer/
+├── app/
+│ └── sb-explorer-ui/ # React frontend
+│ ├── src/
+│ │ ├── api/ # API client and hooks
+│ │ ├── components/ # React components
+│ │ └── routes/ # Page components
+│ └── package.json
+├── src/
+│ └── ServiceBusEmulatorExplorer/ # .NET backend
+│ ├── Endpoints/ # API endpoints
+│ ├── Extensions/ # Service extensions
+│ └── Program.cs # Application entry point
+├── test/
+│ └── ServiceBusEmulatorExplorer.Tests/ # Unit tests
+├── compose.yaml # Docker Compose configuration
+└── sb-explorer.slnx # .NET solution file
+```
+
+## 🐳 Docker
+
+### Build Custom Image
+
+```bash
+docker build -t sb-explorer:local -f src/ServiceBusEmulatorExplorer/Dockerfile .
+```
+
+### Run with Custom Image
+
+Update `compose.yaml` to use your local image:
+
+```yaml
+services:
+ sb-explorer:
+ image: sb-explorer:local
+ # ... rest of configuration
+```
+
+## 🔍 Troubleshooting
+
+### Service Bus Emulator Won't Start
+
+- Ensure Docker is running
+- Check that ports 5672 and 5300 are not in use
+- Verify SQL password meets complexity requirements (uppercase, lowercase, numbers, special characters)
+- Set `ACCEPT_EULA=Y` in your environment
+
+### Connection Issues
+
+- Verify the Service Bus emulator is running: `docker ps`
+- Check connection strings in `appsettings.json`
+- Ensure the emulator is accessible on the configured ports
+
+### Frontend Can't Connect to Backend
+
+- Verify backend is running on the expected port
+- Check `VITE_API_BASE_URL` environment variable
+- Review CORS configuration in `Program.cs`
+
+## 📄 License
+
+The frontend and backend code are licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
+
+## 🤖 Acknowledgements
+
+The majority of the frontend code was written by OpenAI's ChatGPT codex model.
+
+## ⚠️ Limitations
+
+The administration client for the emulator does not expose all of the properties in some of the models, so some parts of the application may not show all available properties or invalid information.
diff --git a/SbExplorer.Mud.csproj b/SbExplorer.Mud.csproj
deleted file mode 100644
index fdd81d1..0000000
--- a/SbExplorer.Mud.csproj
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
- net9.0
- enable
- enable
- Linux
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/SbExplorer.Mud.sln b/SbExplorer.Mud.sln
deleted file mode 100644
index 4ba22dc..0000000
--- a/SbExplorer.Mud.sln
+++ /dev/null
@@ -1,25 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.5.002.0
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SbExplorer.Mud", "SbExplorer.Mud.csproj", "{D2DE19E2-3DB1-4459-ABFD-A6DBFABD1937}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {D2DE19E2-3DB1-4459-ABFD-A6DBFABD1937}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D2DE19E2-3DB1-4459-ABFD-A6DBFABD1937}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D2DE19E2-3DB1-4459-ABFD-A6DBFABD1937}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D2DE19E2-3DB1-4459-ABFD-A6DBFABD1937}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {556E02E5-47C9-43FC-8B25-5596B813EA18}
- EndGlobalSection
-EndGlobal
diff --git a/Services/CachingServiceBusClient.cs b/Services/CachingServiceBusClient.cs
deleted file mode 100644
index 10f812d..0000000
--- a/Services/CachingServiceBusClient.cs
+++ /dev/null
@@ -1,62 +0,0 @@
-using System.Collections.Concurrent;
-using Azure.Core;
-using Azure.Messaging.ServiceBus;
-
-namespace SbExplorer.Mud.Services;
-
-public class CachingServiceBusClient : ServiceBusClient
-{
-
- public CachingServiceBusClient(string connectionString) : base(connectionString)
- {
-
- }
-
- public CachingServiceBusClient(string connectionString, ServiceBusClientOptions options) : base(connectionString, options)
- {
-
- }
-
- public CachingServiceBusClient(string fullyQualifiedNamespace, TokenCredential credential) : base(fullyQualifiedNamespace, credential)
- {
- throw new NotImplementedException("This constructor is not supported");
- }
-
- public CachingServiceBusClient(string fullyQualifiedNamespace, TokenCredential credential, ServiceBusClientOptions options) : base(fullyQualifiedNamespace, credential, options)
- {
- throw new NotImplementedException("This constructor is not supported");
- }
-
- private readonly ConcurrentDictionary _processors = new();
- private readonly ConcurrentDictionary _senders = new();
- private readonly ConcurrentDictionary _receivers = new();
-
-
- public override ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName) => _processors.GetOrAdd($"{topicName}/{subscriptionName}", base.CreateProcessor(topicName, subscriptionName));
-
- public override ServiceBusProcessor CreateProcessor(string queueName, ServiceBusProcessorOptions options) => _processors.GetOrAdd(queueName, base.CreateProcessor(queueName, options));
-
- public override ServiceBusProcessor CreateProcessor(string queueName) => _processors.GetOrAdd(queueName, base.CreateProcessor(queueName));
-
- public override ServiceBusProcessor CreateProcessor(string topicName, string subscriptionName, ServiceBusProcessorOptions options) => _processors.GetOrAdd($"{topicName}/{subscriptionName}", base.CreateProcessor(topicName, subscriptionName, options));
-
- public override ServiceBusReceiver CreateReceiver(string queueName, ServiceBusReceiverOptions options) => _receivers.GetOrAdd(options.SubQueue switch
- {
- SubQueue.DeadLetter => $"{queueName}/deadletter",
- SubQueue.TransferDeadLetter => $"{queueName}/transferdeadletter",
- SubQueue.None => queueName,
- _ => throw new ArgumentOutOfRangeException()
- }, base.CreateReceiver(queueName, options));
-
- public override ServiceBusReceiver CreateReceiver(string topicName, string subscriptionName) => _receivers.GetOrAdd($"{topicName}/{subscriptionName}", base.CreateReceiver(topicName, subscriptionName));
-
- public override ServiceBusReceiver CreateReceiver(string topicName, string subscriptionName, ServiceBusReceiverOptions options) => _receivers.GetOrAdd($"{topicName}/{subscriptionName}", base.CreateReceiver(topicName, subscriptionName, options));
-
- public override ServiceBusReceiver CreateReceiver(string queueName) => _receivers.GetOrAdd(queueName, base.CreateReceiver(queueName));
-
- public override ServiceBusSender CreateSender(string queueName) => _senders.GetOrAdd(queueName, base.CreateSender(queueName));
-
- public override ServiceBusSender CreateSender(string queueOrTopicName, ServiceBusSenderOptions options) => _senders.GetOrAdd(queueOrTopicName, base.CreateSender(queueOrTopicName, options));
-
-
-}
\ No newline at end of file
diff --git a/Services/ServiceBusAdmin.cs b/Services/ServiceBusAdmin.cs
deleted file mode 100644
index dc3a781..0000000
--- a/Services/ServiceBusAdmin.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-using System.Text.Json;
-using Microsoft.Extensions.Caching.Distributed;
-using Microsoft.Extensions.Options;
-using SbExplorer.Mud.Helpers;
-
-namespace SbExplorer.Mud.Services;
-
-public class ServiceBusAdmin(
- IOptions emulatorConfig,
- IDistributedCache cache)
-{
- public async IAsyncEnumerable GetQueues()
- {
- if (await cache.GetAsync(CacheKeyHelper.Queues) is { } queueBytes)
- {
- var queues = JsonSerializer.Deserialize>(queueBytes);
- foreach (var queue in queues!)
- {
- yield return queue;
- }
- }
- }
-
- public async IAsyncEnumerable GetTopics()
- {
- if (await cache.GetAsync(CacheKeyHelper.Topics) is { } bytes)
- {
- var topics = JsonSerializer.Deserialize>(bytes);
- foreach (var topic in topics!)
- {
- yield return topic;
- }
- }
- }
-
- public IEnumerable GetSubscriptions() =>
- from emulatorNamespace in emulatorConfig.Value.UserConfig.Namespaces
- from queue in emulatorNamespace.Topics
- from subscription in queue.Subscriptions
- select new EmulatorSubscription(subscription.Name, queue.Name);
-}
-
-public record EmulatorQueue(string Namespace, string Name, int ActiveMessageCount, int DeadLetterCount);
-
-public record EmulatorTopic(string Name, int ActiveMessageCount, int DeadLetterCount);
-
-public record EmulatorSubscription(string Name, string TopicName);
-
-public record ServiceBusMessageViewModel(long SequenceNumber, string MessageId, DateTimeOffset EnqueuedTime, string State, string BodySize, string LabelSubject, Dictionary Properties, string Body);
\ No newline at end of file
diff --git a/Services/ServiceBusUpdater.cs b/Services/ServiceBusUpdater.cs
deleted file mode 100644
index ec857d1..0000000
--- a/Services/ServiceBusUpdater.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System.Text;
-using System.Text.Json;
-using Azure.Messaging.ServiceBus;
-using Microsoft.Extensions.Caching.Distributed;
-using Microsoft.Extensions.Options;
-using SbExplorer.Mud.Config;
-using SbExplorer.Mud.Helpers;
-
-namespace SbExplorer.Mud.Services;
-
-public class ServiceBusUpdater(
- ServiceBusClient client,
- IOptions emulatorConfig,
- IOptions serviceBusConfig,
- IDistributedCache cache,
- ILogger logger) : BackgroundService
-{
- protected override async Task ExecuteAsync(CancellationToken stoppingToken)
- {
- while (!stoppingToken.IsCancellationRequested)
- {
- try
- {
- logger.LogInformation("Updating queues and topics");
- await Task.WhenAll(UpdateQueues(), UpdateTopics());
- await Task.Delay(TimeSpan.FromMilliseconds(serviceBusConfig.Value.RefreshIntervalMs), stoppingToken);
- }
- catch (Exception e)
- {
- logger.LogError(e, "Error updating queues and topics");
- }
- }
- }
-
-
- private async Task UpdateTopics()
- {
- List result = [];
- foreach (var emulatorNamespace in emulatorConfig.Value.UserConfig.Namespaces)
- {
- foreach (var topic in emulatorNamespace.Topics)
- {
- var receiver = client.CreateReceiver(topic.Name,
- new ServiceBusReceiverOptions { ReceiveMode = ServiceBusReceiveMode.PeekLock });
- var deadLetterReceiver = client.CreateReceiver(topic.Name,
- new ServiceBusReceiverOptions
- { ReceiveMode = ServiceBusReceiveMode.PeekLock, SubQueue = SubQueue.DeadLetter });
- var activeMessages = await receiver.PeekMessagesAsync(1000, long.MinValue);
- var deadLetterMessages = (await deadLetterReceiver.PeekMessagesAsync(1000, long.MinValue));
- var emulatorTopic = new EmulatorTopic(topic.Name, activeMessages.Count, deadLetterMessages.Count);
- result.Add(emulatorTopic);
- }
- }
-
- await cache.SetAsync(CacheKeyHelper.Topics, JsonSerializer.SerializeToUtf8Bytes(result));
- }
-
- private async Task UpdateQueues()
- {
- List result = [];
- foreach (var emulatorNamespace in emulatorConfig.Value.UserConfig.Namespaces)
- {
- foreach (var queue in emulatorNamespace.Queues)
- {
- var receiver = client.CreateReceiver(queue.Name,
- new ServiceBusReceiverOptions
- { ReceiveMode = ServiceBusReceiveMode.PeekLock, SubQueue = SubQueue.None });
- var deadLetterReceiver = client.CreateReceiver(queue.Name,
- new ServiceBusReceiverOptions
- { ReceiveMode = ServiceBusReceiveMode.PeekLock, SubQueue = SubQueue.DeadLetter });
-
- var activeMessages = await receiver.PeekMessagesAsync(1000, long.MinValue);
- var deadLetterMessages = (await deadLetterReceiver.PeekMessagesAsync(1000, long.MinValue));
-
-
- await cache.SetAsync(CacheKeyHelper.QueueKey(emulatorNamespace.Name, queue.Name),
- JsonSerializer.SerializeToUtf8Bytes(activeMessages.Select(CreateViewModel)));
-
- await cache.SetAsync(CacheKeyHelper.DeadLetterQueueKey(emulatorNamespace.Name, queue.Name),
- JsonSerializer.SerializeToUtf8Bytes(deadLetterMessages.Select(CreateViewModel)));
- var emulatorQueue = new EmulatorQueue(emulatorNamespace.Name, queue.Name, activeMessages.Count,
- deadLetterMessages.Count);
- result.Add(emulatorQueue);
- }
- }
-
- await cache.SetAsync(CacheKeyHelper.Queues, JsonSerializer.SerializeToUtf8Bytes(result));
- }
-
- private static ServiceBusMessageViewModel CreateViewModel(ServiceBusReceivedMessage m) =>
- new(m.SequenceNumber, m.MessageId, m.EnqueuedTime, m.State.ToString(),
- Encoding.Default.GetByteCount(m.Body.ToString()).ToString(), m.Subject,
- m.ApplicationProperties.ToDictionary(), m.Body.ToString());
-}
\ No newline at end of file
diff --git a/app/sb-explorer-ui/.gitignore b/app/sb-explorer-ui/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/app/sb-explorer-ui/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/app/sb-explorer-ui/README.md b/app/sb-explorer-ui/README.md
new file mode 100644
index 0000000..d2e7761
--- /dev/null
+++ b/app/sb-explorer-ui/README.md
@@ -0,0 +1,73 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/app/sb-explorer-ui/eslint.config.js b/app/sb-explorer-ui/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/app/sb-explorer-ui/eslint.config.js
@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/app/sb-explorer-ui/index.html b/app/sb-explorer-ui/index.html
new file mode 100644
index 0000000..8be4655
--- /dev/null
+++ b/app/sb-explorer-ui/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ sb-explorer-ui
+
+
+
+
+
+
diff --git a/app/sb-explorer-ui/package-lock.json b/app/sb-explorer-ui/package-lock.json
new file mode 100644
index 0000000..3224da8
--- /dev/null
+++ b/app/sb-explorer-ui/package-lock.json
@@ -0,0 +1,5881 @@
+{
+ "name": "sb-explorer-ui",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "sb-explorer-ui",
+ "version": "0.0.0",
+ "dependencies": {
+ "@fluentui/react-components": "^9.72.9",
+ "@fluentui/react-icons": "^2.0.316",
+ "@mantine/core": "^8.3.12",
+ "@mantine/hooks": "^8.3.12",
+ "@mantine/notifications": "^8.3.12",
+ "@monaco-editor/react": "^4.7.0",
+ "@tabler/icons-react": "^3.36.1",
+ "@tanstack/react-query": "^5.90.18",
+ "@tanstack/react-query-devtools": "^5.91.2",
+ "axios": "^1.13.2",
+ "monaco-editor": "^0.55.1",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.12.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "cross-env": "^10.1.0",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.4"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
+ "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
+ "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
+ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/generator": "^7.28.6",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
+ "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
+ "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.6"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+ "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
+ "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/generator": "^7.28.6",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.6",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
+ "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+ "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
+ "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
+ "license": "MIT"
+ },
+ "node_modules/@epic-web/invariant": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
+ "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
+ "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
+ "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
+ "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/devtools": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.2.3.tgz",
+ "integrity": "sha512-ZTcxTvgo9CRlP7vJV62yCxdqmahHTGpSTi5QaTDgGoyQq0OyjaVZhUhXv/qdkQFOI3Sxlfmz0XGG4HaZMsDf8Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react": {
+ "version": "0.27.16",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz",
+ "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.1.6",
+ "@floating-ui/utils": "^0.2.10",
+ "tabbable": "^6.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=17.0.0",
+ "react-dom": ">=17.0.0"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
+ "node_modules/@fluentui/keyboard-keys": {
+ "version": "9.0.8",
+ "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.8.tgz",
+ "integrity": "sha512-iUSJUUHAyTosnXK8O2Ilbfxma+ZyZPMua5vB028Ys96z80v+LFwntoehlFsdH3rMuPsA8GaC1RE7LMezwPBPdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.1"
+ }
+ },
+ "node_modules/@fluentui/priority-overflow": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.2.1.tgz",
+ "integrity": "sha512-WH5dv54aEqWo/kKQuADAwjv66W6OUMFllQMjpdkrktQp7pu4JXtmF60iYcp9+iuIX9iCeW01j8gNTU08MQlfIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.1"
+ }
+ },
+ "node_modules/@fluentui/react-accordion": {
+ "version": "9.8.15",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.8.15.tgz",
+ "integrity": "sha512-/KMZKD97C6hvRUF4S/GiMaguFh2VWHAm0z58y++Si9drmgTvpAUHxXKHELxnZFYKLS76Gc0gMXnKrPMlp0wDkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-motion": "^9.11.5",
+ "@fluentui/react-motion-components-preview": "^0.14.2",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-alert": {
+ "version": "9.0.0-beta.131",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.131.tgz",
+ "integrity": "sha512-mpt5uMuAjUG/J6T0yq/r54pwhVl/D/lk/OLF3ovhYzWuiNhEOinwx2b81fK02Rm/K3i4sl25QX4h19Aie5NLKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-avatar": "^9.9.13",
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-icons": "^2.0.239",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-aria": {
+ "version": "9.17.7",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.17.7.tgz",
+ "integrity": "sha512-OsPKp6BmE+W73UNMM7JX6WNQa5H4/oFKgt/BAQxp9mhM6lYw4Skmf9ZLn0vBccFuc0wh2hYDuMgKQ2/2uTUfow==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-avatar": {
+ "version": "9.9.13",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.9.13.tgz",
+ "integrity": "sha512-a8eVQ2WYiGQvV7BVzcMXGkpZHfNzduC8S74ux5cMbeDuFG8JH8XKBIgOErAxQwFt0wATqyISelo5vn176sQwmw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-badge": "^9.4.12",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-popover": "^9.12.13",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-tooltip": "^9.8.12",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-badge": {
+ "version": "9.4.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.4.12.tgz",
+ "integrity": "sha512-N7B3l3PGH1HKzjvXBmnElyTpd7JIIimuxEWSu6v+4Jas3UCbbEjv6DfhmEOLeBFle09q3ILTJ/Hf7t9jhEAyyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-breadcrumb": {
+ "version": "9.3.14",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.3.14.tgz",
+ "integrity": "sha512-KfMXejIEWA5VWPkp0lJIN18qqlf/3TpwnkBafRCxeeVx5dVuT6z2PW5bxJiDQ1jRSpmYiGzs3MkJOnlWuMdLhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-link": "^9.7.1",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-button": {
+ "version": "9.7.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.7.1.tgz",
+ "integrity": "sha512-nPrsnORTrf4Hy4uZTxULgUmqd1hQK3ZorDfIYhzcbnBnn78+9zl9NyKQI0SqKxM8jG16FuK8jgrpHLiYq/8PSA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-card": {
+ "version": "9.5.8",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.5.8.tgz",
+ "integrity": "sha512-nS/q3Vw2AqAOhKTOxgwU0xgE4neFB9OT+9fK/OuwmvgFLvkV5in/oszod+QlqJzarn3hTp1avWlSOItswPoyOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-text": "^9.6.12",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-carousel": {
+ "version": "9.9.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-carousel/-/react-carousel-9.9.0.tgz",
+ "integrity": "sha512-EaiEe1oT9lFrIZfBfgF046h+2qcwKQZUJcc0Rv7yFDyWkNXrdM1YKG+q89V+D7P3z8tJYXKsNy4+tpFc/xgrKg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-tooltip": "^9.8.12",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1",
+ "embla-carousel": "^8.5.1",
+ "embla-carousel-autoplay": "^8.5.1",
+ "embla-carousel-fade": "^8.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-checkbox": {
+ "version": "9.5.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.5.12.tgz",
+ "integrity": "sha512-km1itgOZJ/Io1/F9wLMp9yHgfgyM1HnYBKJjUD4+H+wkdVoF7ZsjWls2s8tB2EMvsbWRBqgPH80yCMNsGyipjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-label": "^9.3.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-color-picker": {
+ "version": "9.2.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-color-picker/-/react-color-picker-9.2.12.tgz",
+ "integrity": "sha512-fToyincQFiuYxzfIMii9M4A55taEFtQ0DzDZPlyIi45j/39eSmlwGzBDfFq7KKvVqGHvZKCKcSymUlxA+PPEcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^3.3.4",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-combobox": {
+ "version": "9.16.13",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.16.13.tgz",
+ "integrity": "sha512-FavYGlTKOBED44h6d587Ic1AVi9/eqEh+B2Xph7EujCvq9ZFtjYPtZVDcgEuAZd/C6QY5vrFoZ5+abjLqal1bg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-positioning": "^9.20.11",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-components": {
+ "version": "9.72.9",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.72.9.tgz",
+ "integrity": "sha512-yiNzCjPixUhYokf8kgl0ItXQ/smPceFvz9XP73z0Tp0dRNzRQG20dK0Oz3w+7vnOt9VmnAH9KGNRXqNAY+CPdg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-accordion": "^9.8.15",
+ "@fluentui/react-alert": "9.0.0-beta.131",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-avatar": "^9.9.13",
+ "@fluentui/react-badge": "^9.4.12",
+ "@fluentui/react-breadcrumb": "^9.3.14",
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-card": "^9.5.8",
+ "@fluentui/react-carousel": "^9.9.0",
+ "@fluentui/react-checkbox": "^9.5.12",
+ "@fluentui/react-color-picker": "^9.2.12",
+ "@fluentui/react-combobox": "^9.16.13",
+ "@fluentui/react-dialog": "^9.16.5",
+ "@fluentui/react-divider": "^9.5.1",
+ "@fluentui/react-drawer": "^9.11.1",
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-image": "^9.3.12",
+ "@fluentui/react-infobutton": "9.0.0-beta.108",
+ "@fluentui/react-infolabel": "^9.4.13",
+ "@fluentui/react-input": "^9.7.12",
+ "@fluentui/react-label": "^9.3.12",
+ "@fluentui/react-link": "^9.7.1",
+ "@fluentui/react-list": "^9.6.7",
+ "@fluentui/react-menu": "^9.20.6",
+ "@fluentui/react-message-bar": "^9.6.16",
+ "@fluentui/react-motion": "^9.11.5",
+ "@fluentui/react-nav": "^9.3.16",
+ "@fluentui/react-overflow": "^9.6.6",
+ "@fluentui/react-persona": "^9.5.13",
+ "@fluentui/react-popover": "^9.12.13",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-positioning": "^9.20.11",
+ "@fluentui/react-progress": "^9.4.12",
+ "@fluentui/react-provider": "^9.22.12",
+ "@fluentui/react-radio": "^9.5.12",
+ "@fluentui/react-rating": "^9.3.12",
+ "@fluentui/react-search": "^9.3.12",
+ "@fluentui/react-select": "^9.4.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-skeleton": "^9.4.12",
+ "@fluentui/react-slider": "^9.5.12",
+ "@fluentui/react-spinbutton": "^9.5.12",
+ "@fluentui/react-spinner": "^9.7.12",
+ "@fluentui/react-swatch-picker": "^9.4.12",
+ "@fluentui/react-switch": "^9.5.1",
+ "@fluentui/react-table": "^9.19.6",
+ "@fluentui/react-tabs": "^9.10.8",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-tag-picker": "^9.7.14",
+ "@fluentui/react-tags": "^9.7.13",
+ "@fluentui/react-teaching-popover": "^9.6.14",
+ "@fluentui/react-text": "^9.6.12",
+ "@fluentui/react-textarea": "^9.6.12",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-toast": "^9.7.10",
+ "@fluentui/react-toolbar": "^9.6.14",
+ "@fluentui/react-tooltip": "^9.8.12",
+ "@fluentui/react-tree": "^9.15.8",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@fluentui/react-virtualizer": "9.0.0-alpha.108",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-context-selector": {
+ "version": "9.2.13",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.2.13.tgz",
+ "integrity": "sha512-Jzo4aDzGHh131wub7XqDaaZB2V+kd90HgpvFHdtBenL8LjDVxuSYpuHlqVF+Lu1mQBDu4V8JQS6KiYLv9xFp8g==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-utilities": "^9.26.0",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0",
+ "scheduler": ">=0.19.0"
+ }
+ },
+ "node_modules/@fluentui/react-dialog": {
+ "version": "9.16.5",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.16.5.tgz",
+ "integrity": "sha512-5MogBImDZ/qXY2ShXAJBbC9XFRwgxDU7lbe31DcD1RLJYV+zXbXIXbMNvTCtSFc3qKRORZgWiYJidR9zb4MiwA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-motion": "^9.11.5",
+ "@fluentui/react-motion-components-preview": "^0.14.2",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-divider": {
+ "version": "9.5.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.5.1.tgz",
+ "integrity": "sha512-bWc1gbHYqT3werzx+Suw0rBJfn6+bMtmZ8PDy4UIg/Fn06oPum4IqgHn3r9HpQtmphhspBGrI/q2BD/YWEHAyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-drawer": {
+ "version": "9.11.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.11.1.tgz",
+ "integrity": "sha512-xGbiGCc0j7smvet+ZbGCl9yrnk9WDVxD1RN7egO6CXZ6qRurE76AX/9dtnw22/Md+HPkzOmNAw95A0LOYUg04g==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-dialog": "^9.16.5",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-motion": "^9.11.5",
+ "@fluentui/react-motion-components-preview": "^0.14.2",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-field": {
+ "version": "9.4.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.4.12.tgz",
+ "integrity": "sha512-GJq/SbXXAduKUJK8XpIphfGLNgBZm2fizxZt0pKttE4HkBjFbHaBbEkjlNZc8S+2d8ec0adkqx9hwC9OnqZMUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-label": "^9.3.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-icons": {
+ "version": "2.0.316",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.316.tgz",
+ "integrity": "sha512-tZPOtsUmoOrgLeM/rLjkzLlWOEmIghXNh/DYQzm5RD/Q4epklOzjnsFvc/Mn2tuXiVxi+vvXxsQp21E1aLpmWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@griffel/react": "^1.0.0",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-image": {
+ "version": "9.3.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.3.12.tgz",
+ "integrity": "sha512-S02tX0s5UrWY0MyVfkq8P/3vyyAZ6LPdFAwjy2dWIWoEpYA2XH+fCDDsnPSThSZs6IUKUqgN/BpXW0/lsPcCuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-infobutton": {
+ "version": "9.0.0-beta.108",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.108.tgz",
+ "integrity": "sha512-mXwi5LuVNJK66HxOid4mzZaV571E3ZmyKDK8BG0Bd+nErTixc0H6D3kPIxgBbN4RaZjurPkovg5vluAYAzMgxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.237",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-label": "^9.3.12",
+ "@fluentui/react-popover": "^9.12.13",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-infolabel": {
+ "version": "9.4.13",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.4.13.tgz",
+ "integrity": "sha512-szas/IPeg3XETtxily/9muYM9/czky+CVuntdbhHaCGyg1YZ1xMbRhXgaGUpJtBnOuCaLQV4wcX+r6bCYkN95A==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-label": "^9.3.12",
+ "@fluentui/react-popover": "^9.12.13",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-input": {
+ "version": "9.7.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.7.12.tgz",
+ "integrity": "sha512-91h/J6xsH4hRrtclPL0sEU2zdAfs2t2IpDz+AWwJ7LTWn+DfxNjr4ItncbBC8DCB69IoKOmNma/Hup/4LaCsMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-jsx-runtime": {
+ "version": "9.3.4",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.3.4.tgz",
+ "integrity": "sha512-socz8H63f7CBYECzBkeeZGUAGgPDvsr4kZRHQoQw5eXBKlSb+08p7F7Zdq0hYAPQhTgXoxH1DZ4JlXzCCmweVg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-utilities": "^9.26.0",
+ "@swc/helpers": "^0.5.1",
+ "react-is": "^17.0.2"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-label": {
+ "version": "9.3.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.3.12.tgz",
+ "integrity": "sha512-drVHXtiK/uhWF83lbeGm+z4r2IBVA8Zp6+VXD5lsR0nJ6o9v2TubJDTgOpgpWMaFDPDSHUO7jCAqwNdzQ3lpsw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-link": {
+ "version": "9.7.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.7.1.tgz",
+ "integrity": "sha512-OkFR95N8D1KQPmz4eZPu+mei79JNYjURLythuNfgvLG3SgNpOKfT7b5hzhUCafzEB1e6Oviw/nGF99t65pfdMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-list": {
+ "version": "9.6.7",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-list/-/react-list-9.6.7.tgz",
+ "integrity": "sha512-/vUcP6QeUrVuVVZGab+W/a66O/7RxbqErt9S3teC90X8e5Bq0Nb7Q1aeiC4gyQr1XvwzKGKhqe/3srU8X+54Qw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-checkbox": "^9.5.12",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-menu": {
+ "version": "9.20.6",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.20.6.tgz",
+ "integrity": "sha512-AsbtrJigDeMlVJbIZMHDjNrW2DFe0hzgEN4/Dc/fYaHqOFIe1OazNAWZl4dsXyEHZxkCo791X5jhR12gvBDbcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-positioning": "^9.20.11",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-message-bar": {
+ "version": "9.6.16",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.6.16.tgz",
+ "integrity": "sha512-yg1vSYLDaTKwDeia2t1ivngBy7sinx4McBjyX8l8pUaAdrT+OqDcDeevXpFNZ0/0eA2a3BVJ6qbu4iab1d9FPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-link": "^9.7.1",
+ "@fluentui/react-motion": "^9.11.5",
+ "@fluentui/react-motion-components-preview": "^0.14.2",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-motion": {
+ "version": "9.11.5",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-motion/-/react-motion-9.11.5.tgz",
+ "integrity": "sha512-o4rTgeQbxER4tZ47eZ+ej/uy9iUNvQtB5fF55+8G00beBSX2acwmslb/GJOOw/mnkcB14Hoa6f8LU2JabYNXSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-motion-components-preview": {
+ "version": "0.14.2",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-motion-components-preview/-/react-motion-components-preview-0.14.2.tgz",
+ "integrity": "sha512-QbdbgzcM02AvYCN4PbBMZCw10vMh9AvPK8kK2kbMdNWXolbRau2ndNVfXpXvZxY9KZFc2lJlYUBLWJTLDINQXA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-motion": "*",
+ "@fluentui/react-utilities": "*",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-nav": {
+ "version": "9.3.16",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-nav/-/react-nav-9.3.16.tgz",
+ "integrity": "sha512-qoPfC/pAYDZQxAhfFhzP6a5QH/1lafmOWNXLrZxX5DadGl9mg9Tr6/t6rcP/ZuJSTHGzVX1IUmxboc+z62gcww==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-divider": "^9.5.1",
+ "@fluentui/react-drawer": "^9.11.1",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-motion": "^9.11.5",
+ "@fluentui/react-motion-components-preview": "^0.14.2",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-tooltip": "^9.8.12",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-overflow": {
+ "version": "9.6.6",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.6.6.tgz",
+ "integrity": "sha512-iXXEQCSNn6xfzzUrEURplq7uc+OrxTvU6EbWVeFxCQnwmbnEJlmxtFzWTS4XHR1Z00Z+lZ4pCUxD1q7DH9926Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/priority-overflow": "^9.2.1",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-persona": {
+ "version": "9.5.13",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.5.13.tgz",
+ "integrity": "sha512-H2gUXRp3U28szgjMskKRM0OI1TvEaZ9LJwvCo2aEf03ijvWVeJYSg8Q3XLmglrAbjENRWIR7/kZg2r8Hd0vlvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-avatar": "^9.9.13",
+ "@fluentui/react-badge": "^9.4.12",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-popover": {
+ "version": "9.12.13",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.12.13.tgz",
+ "integrity": "sha512-hb1G/zLCfoD4fUHwPLZ7Qqwaoqm5nk8dyV8s491J3tpKhifce+cVgqA2/5MYMcZeo07QRIzn5oZ10t7QZCBOKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-positioning": "^9.20.11",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-portal": {
+ "version": "9.8.9",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.8.9.tgz",
+ "integrity": "sha512-zmaEPXwSLMmCzRlKQUZ+ZZqNjGe+h6K+Gz4NIFuz+jVbCRpOPEfumaoE6oy9wRITQFHq3DQrkPSRQxrZ7oUHRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-positioning": {
+ "version": "9.20.11",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.20.11.tgz",
+ "integrity": "sha512-LjLQiIZw9wM7OSSi1CesrV6yvmJTsLFOMA8jypglm4GoPCXf4BzD7bEk55fgJYBGfa1YQNGMbv2LlFqmNOGrQQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/devtools": "^0.2.3",
+ "@floating-ui/dom": "^1.6.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-progress": {
+ "version": "9.4.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.4.12.tgz",
+ "integrity": "sha512-CGlk1yXhT6hBDbjgYyk+qgKbuU089iwYeueiYit5TLFb0LUUjfWjdcex7s73Qa+Obyss5MeHun8DQwX9Ve/FoQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-provider": {
+ "version": "9.22.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.22.12.tgz",
+ "integrity": "sha512-GhNd18zORZ/7m37TjF3UTKAJCfRgCXZi3PcdoI5SvseR3SPWl93R8mYi0SDCe6tIw7TNgzCn6fS7X6O+hAV+rA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/core": "^1.16.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-radio": {
+ "version": "9.5.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.5.12.tgz",
+ "integrity": "sha512-T0UdYn8comjc05SyZc37Cx8QT6ZhdGr/0az+ygK15uutRrj6ZQJV+xYAOo8rEwu5P51tD077nV8A9k1asf0TAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-label": "^9.3.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-rating": {
+ "version": "9.3.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-rating/-/react-rating-9.3.12.tgz",
+ "integrity": "sha512-q8P0sQ5b5EPNLJZH6jN37avhZkm5aHPmaE4btOHMsAYivh5CMtQfgsBZ5vO/z6acXTdWV+r5DoF1gKIMdwEtrA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-search": {
+ "version": "9.3.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-search/-/react-search-9.3.12.tgz",
+ "integrity": "sha512-F1qvEaoeLh4aYTbRXI5gOb63EFjBTVBeb084RKAYAzFBaiv7w4nUdPAuyK6+mevtO+wSdUHvb9HFwrxkLpY05w==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-input": "^9.7.12",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-select": {
+ "version": "9.4.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.4.12.tgz",
+ "integrity": "sha512-IwIc9qGNTmgMC/zP05mempBSaZWoSG3JknOoQjoFVpi6sOL4pw/1L2f2fH7DvnNQtWymFuXt9jEpJdI2xKPVTA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-shared-contexts": {
+ "version": "9.26.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.26.0.tgz",
+ "integrity": "sha512-r52B+LUevs930pe45pFsppM9XNvY+ojgRgnDE+T/6aiwR/Mo4YoGrtjhLEzlQBeTGuySICTeaAiXfuH6Keo5Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-theme": "^9.2.0",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-skeleton": {
+ "version": "9.4.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.4.12.tgz",
+ "integrity": "sha512-aOaoOn4L3SMqGW83GmvGrRrv6TnT0uuxsDk6/mSfPW7P9QwhaZZQRiBiymH01RYSMBF9J3DFgZzKsKqVihts0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-slider": {
+ "version": "9.5.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.5.12.tgz",
+ "integrity": "sha512-zfMyC0+ytNMtZEtqVXg+8l8dRrXAfRccPxofngZzHiVgLknMlc7L9jjWBYOGiB4VbO1XR/+D7/KrsjBf0xvXyA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-spinbutton": {
+ "version": "9.5.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.5.12.tgz",
+ "integrity": "sha512-+t7GOyJkaevduT6CYEX9PLlsdPnJKWeXP6Va1Ml2wFnDz8RtJTTqzbedSqmk8CLpwbZ8+/Ix40pIbp+9Q5v2Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-spinner": {
+ "version": "9.7.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.7.12.tgz",
+ "integrity": "sha512-8jTG1DTKipkpkaNwl9uxDs8yMKMK8ogzYrMMbNR1pfYVtpiDSfwxwZIXTqh9r1vS4SU3WnFQ0irRu1tIIumAnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-label": "^9.3.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-swatch-picker": {
+ "version": "9.4.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-swatch-picker/-/react-swatch-picker-9.4.12.tgz",
+ "integrity": "sha512-c3OHBbPNneQLm+A9rzVaU757FPTBog+tYQU7nnmHlM0LZSTIhJf1XRBsLGNSnqmlAzLc94PjW/867SstQ+vuaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-switch": {
+ "version": "9.5.1",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.5.1.tgz",
+ "integrity": "sha512-fa9EKNyssYwrkbWQn3CQ4IfnsVy+ttiRWom+s9eJDtM9NTtLZMJpei0Ve6vCD27SIbwBJhngWLe7j5/HeAg0uQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-label": "^9.3.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-table": {
+ "version": "9.19.6",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.19.6.tgz",
+ "integrity": "sha512-LKGuFnYfknmaFCH35T0VjgbeaQIfg5SCVPgnNGKHDmNd85QvOR5AG7CMBm0LSltjZW6NFHblkRmnOkF2AkPucQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-avatar": "^9.9.13",
+ "@fluentui/react-checkbox": "^9.5.12",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-radio": "^9.5.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tabs": {
+ "version": "9.10.8",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.10.8.tgz",
+ "integrity": "sha512-Msxd4Ajhu+YZW7Iv5WQZBr2yynsOkwQjXkSH28ObjAZ/rFkb2Iq9uXvSAFJHba++Ecz1i2tchAsELWqT9oyLxA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tabster": {
+ "version": "9.26.11",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.26.11.tgz",
+ "integrity": "sha512-x2UjXowknK4gHJT14ezIeaLAKozZrpqsvWj8Mqa6p+TiOdHyo8YO6mecpCV1QWyz86qYsOPYhK/i0MSapwaELA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1",
+ "keyborg": "^2.6.0",
+ "tabster": "^8.5.5"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tag-picker": {
+ "version": "9.7.14",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tag-picker/-/react-tag-picker-9.7.14.tgz",
+ "integrity": "sha512-SMrLFkuVdZ/UPLHhumodQcM/V4uxkS3GayCBykddn1OWtWGVLjN4idCes56XGdZyNq79u4BEu7Vtxwucjv3oXg==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-combobox": "^9.16.13",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-positioning": "^9.20.11",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-tags": "^9.7.13",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tags": {
+ "version": "9.7.13",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.7.13.tgz",
+ "integrity": "sha512-lg6C4b0RZKroQROSyezrLusR8/p/W6poQyKrJSEigiYhGZUm32Z+oi7qS7FDahVV/DA2vpRnuY/IfclIDszvTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-avatar": "^9.9.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-teaching-popover": {
+ "version": "9.6.14",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-teaching-popover/-/react-teaching-popover-9.6.14.tgz",
+ "integrity": "sha512-3FRyaoRSO/XJGiOJxRe1E7bdDPr8KZEX/Dp/IYRn45Y2War308sscaUUPz0N3ut9iRQlT2edsHSlBMNprLEXRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-popover": "^9.12.13",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1",
+ "use-sync-external-store": "^1.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8.0 <20.0.0",
+ "@types/react-dom": ">=16.8.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-text": {
+ "version": "9.6.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.6.12.tgz",
+ "integrity": "sha512-IYiyYflw3ozS2Kil93vIqgu4JAJvFLswldJ5oBgBVOAM+MGG7G7He7Dp9tVRYxqHxkA54Um5Mv3HcUUgJ5sqww==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-textarea": {
+ "version": "9.6.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.6.12.tgz",
+ "integrity": "sha512-xoRYQpc76qc0WsAlOKhygnhZActTbbPvNdQU12R6bk6P4fUPBgX6rNMsNv6cVSr3ZvPuWn3bQq80PjPO10iezA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-field": "^9.4.12",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-theme": {
+ "version": "9.2.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.2.0.tgz",
+ "integrity": "sha512-Q0zp/MY1m5RjlkcwMcjn/PQRT2T+q3bgxuxWbhgaD07V+tLzBhGROvuqbsdg4YWF/IK21zPfLhmGyifhEu0DnQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/tokens": "1.0.0-alpha.22",
+ "@swc/helpers": "^0.5.1"
+ }
+ },
+ "node_modules/@fluentui/react-toast": {
+ "version": "9.7.10",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.7.10.tgz",
+ "integrity": "sha512-Zvh/19VpFXft7VFvlHEyURg766RyKBE6eekrmtgE416ow07pfn1a7X7VqTyfp90uEaJsowB//twJNjCc3r3oAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-motion": "^9.11.5",
+ "@fluentui/react-motion-components-preview": "^0.14.2",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-toolbar": {
+ "version": "9.6.14",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.6.14.tgz",
+ "integrity": "sha512-wjUqbfNSGlmgpMsJvpd8C7qzXUav3pb88ctyzziweURZskOMAIx8wv0PHUih9h9haMB5ayTiLuJL4Lcpv6jNlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-divider": "^9.5.1",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-radio": "^9.5.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tooltip": {
+ "version": "9.8.12",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.8.12.tgz",
+ "integrity": "sha512-ZA36KqmGWhK1HmNd1HO5p3Fz3cM06p/1kSKEB6b+F2opY+Db8IQGa6ER8wVtxLnUs/WFrcjJPcy7DuD2oyeSFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-portal": "^9.8.9",
+ "@fluentui/react-positioning": "^9.20.11",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-tree": {
+ "version": "9.15.8",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.15.8.tgz",
+ "integrity": "sha512-T2USjFQ2tPb0TzX3FagifQzJKYGq0T8IQYHdfHO7LP7sThI13Mnt6ke7mGC3SOPi8WKUCMRaoXAksbggUMXFUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-aria": "^9.17.7",
+ "@fluentui/react-avatar": "^9.9.13",
+ "@fluentui/react-button": "^9.7.1",
+ "@fluentui/react-checkbox": "^9.5.12",
+ "@fluentui/react-context-selector": "^9.2.13",
+ "@fluentui/react-icons": "^2.0.245",
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-motion": "^9.11.5",
+ "@fluentui/react-motion-components-preview": "^0.14.2",
+ "@fluentui/react-radio": "^9.5.12",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-tabster": "^9.26.11",
+ "@fluentui/react-theme": "^9.2.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-utilities": {
+ "version": "9.26.0",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.26.0.tgz",
+ "integrity": "sha512-3i/Vdt9UzDs/vuQvdR6HJFMhkOqB22lOGJ+v6VpkjGO81ywnQwP4LKkaKK534q+qiVbcKumCkHOeRhtMAUJXPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/keyboard-keys": "^9.0.8",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/react-virtualizer": {
+ "version": "9.0.0-alpha.108",
+ "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.108.tgz",
+ "integrity": "sha512-2uaGDhGbVZqBd/INh2tiSefVUwdAPK/PDJ8e0pJ34+N77A1Mcq9eSbyaBp5GLZ/GcycHAWnnyDCall9Avpqo6g==",
+ "license": "MIT",
+ "dependencies": {
+ "@fluentui/react-jsx-runtime": "^9.3.4",
+ "@fluentui/react-shared-contexts": "^9.26.0",
+ "@fluentui/react-utilities": "^9.26.0",
+ "@griffel/react": "^1.5.32",
+ "@swc/helpers": "^0.5.1"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.14.0 <20.0.0",
+ "@types/react-dom": ">=16.9.0 <20.0.0",
+ "react": ">=16.14.0 <20.0.0",
+ "react-dom": ">=16.14.0 <20.0.0"
+ }
+ },
+ "node_modules/@fluentui/tokens": {
+ "version": "1.0.0-alpha.22",
+ "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.22.tgz",
+ "integrity": "sha512-i9fgYyyCWFRdUi+vQwnV6hp7wpLGK4p09B+O/f2u71GBXzPuniubPYvrIJYtl444DD6shLjYToJhQ1S6XTFwLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@swc/helpers": "^0.5.1"
+ }
+ },
+ "node_modules/@griffel/core": {
+ "version": "1.19.2",
+ "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.19.2.tgz",
+ "integrity": "sha512-WkB/QQkjy9dE4vrNYGhQvRRUHFkYVOuaznVOMNTDT4pS9aTJ9XPrMTXXlkpcwaf0D3vNKoerj4zAwnU2lBzbOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@emotion/hash": "^0.9.0",
+ "@griffel/style-types": "^1.3.0",
+ "csstype": "^3.1.3",
+ "rtl-css-js": "^1.16.1",
+ "stylis": "^4.2.0",
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@griffel/react": {
+ "version": "1.5.32",
+ "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.32.tgz",
+ "integrity": "sha512-jN3SmSwAUcWFUQuQ9jlhqZ5ELtKY21foaUR0q1mJtiAeSErVgjkpKJyMLRYpvaFGWrDql0Uz23nXUogXbsS2wQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@griffel/core": "^1.19.2",
+ "tslib": "^2.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@griffel/style-types": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.3.0.tgz",
+ "integrity": "sha512-bHwD3sUE84Xwv4dH011gOKe1jul77M1S6ZFN9Tnq8pvZ48UMdY//vtES6fv7GRS5wXYT4iqxQPBluAiYAfkpmw==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.1.3"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@mantine/core": {
+ "version": "8.3.12",
+ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.12.tgz",
+ "integrity": "sha512-bDEoUl4SneltfI1GeEaBk6BVDbLuB/w15YwseAmUvc8ldAbNcsVhxKxY/BdhwqUo6O3L2vhdlb3WwxR1y8741g==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react": "^0.27.16",
+ "clsx": "^2.1.1",
+ "react-number-format": "^5.4.4",
+ "react-remove-scroll": "^2.7.1",
+ "react-textarea-autosize": "8.5.9",
+ "type-fest": "^4.41.0"
+ },
+ "peerDependencies": {
+ "@mantine/hooks": "8.3.12",
+ "react": "^18.x || ^19.x",
+ "react-dom": "^18.x || ^19.x"
+ }
+ },
+ "node_modules/@mantine/hooks": {
+ "version": "8.3.12",
+ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.12.tgz",
+ "integrity": "sha512-lMMDzDewd3lUNtJCAHDj3g8On9X5aBl4q6EBwgOixKQSby9RG9ASEpK8oYHundHTm9tzo3MDeXWV/z32oSQWuw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.x || ^19.x"
+ }
+ },
+ "node_modules/@mantine/notifications": {
+ "version": "8.3.12",
+ "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.3.12.tgz",
+ "integrity": "sha512-lqPPa11XdcndG8ywMao8yVkMA4jg/pBNbS85bR7OwHQa1yUftmfVlqJl9PZZCiWLX2AgKY3+xM5dHo4LidL+DA==",
+ "license": "MIT",
+ "dependencies": {
+ "@mantine/store": "8.3.12",
+ "react-transition-group": "4.4.5"
+ },
+ "peerDependencies": {
+ "@mantine/core": "8.3.12",
+ "@mantine/hooks": "8.3.12",
+ "react": "^18.x || ^19.x",
+ "react-dom": "^18.x || ^19.x"
+ }
+ },
+ "node_modules/@mantine/store": {
+ "version": "8.3.12",
+ "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.3.12.tgz",
+ "integrity": "sha512-EC4eIKpm5s7neMbBrWsP6jGKLqrzHf63Ao3penYr7fn25dFXdbXZYw+IG8GYzxOC4yG61b2zTS+bpy5+vwzXpw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.x || ^19.x"
+ }
+ },
+ "node_modules/@monaco-editor/loader": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
+ "license": "MIT",
+ "dependencies": {
+ "state-local": "^1.0.6"
+ }
+ },
+ "node_modules/@monaco-editor/react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@monaco-editor/loader": "^1.5.0"
+ },
+ "peerDependencies": {
+ "monaco-editor": ">= 0.25.0 < 1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.53",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
+ "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+ "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
+ "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
+ "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
+ "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
+ "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
+ "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
+ "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
+ "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
+ "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
+ "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
+ "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
+ "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
+ "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
+ "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
+ "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
+ "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
+ "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
+ "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
+ "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
+ "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
+ "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
+ "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@swc/helpers": {
+ "version": "0.5.18",
+ "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz",
+ "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@tabler/icons": {
+ "version": "3.36.1",
+ "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.36.1.tgz",
+ "integrity": "sha512-f4Jg3Fof/Vru5ioix/UO4GX+sdDsF9wQo47FbtvG+utIYYVQ/QVAC0QYgcBbAjQGfbdOh2CCf0BgiFOF9Ixtjw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ }
+ },
+ "node_modules/@tabler/icons-react": {
+ "version": "3.36.1",
+ "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.36.1.tgz",
+ "integrity": "sha512-/8nOXeNeMoze9xY/QyEKG65wuvRhkT3q9aytaur6Gj8bYU2A98YVJyLc9MRmc5nVvpy+bRlrrwK/Ykr8WGyUWg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tabler/icons": ""
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ },
+ "peerDependencies": {
+ "react": ">= 16"
+ }
+ },
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.18",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.18.tgz",
+ "integrity": "sha512-rbGx6bHgPNVzutP7BEr+53UPKohpckqlMAad+To9UxTbeaQ+kC/1SDRj+QzkwbQ7qhLT/1IKp34yS6thda6fzA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/query-devtools": {
+ "version": "5.92.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.92.0.tgz",
+ "integrity": "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.18",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.18.tgz",
+ "integrity": "sha512-KqNZX0C5IFz4639zR1ilnQ288tQdJrMNLtzmlzyJ14xauBkhtLEy3mPU/V4KiHsr41eL1ILZbDP36TB12lYfCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@tanstack/react-query-devtools": {
+ "version": "5.91.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.2.tgz",
+ "integrity": "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-devtools": "5.92.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "^5.90.14",
+ "react": "^18 || ^19"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.10.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz",
+ "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
+ "integrity": "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/type-utils": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.53.0",
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.0.tgz",
+ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.0.tgz",
+ "integrity": "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.53.0",
+ "@typescript-eslint/types": "^8.53.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz",
+ "integrity": "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz",
+ "integrity": "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz",
+ "integrity": "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.0.tgz",
+ "integrity": "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz",
+ "integrity": "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.53.0",
+ "@typescript-eslint/tsconfig-utils": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/visitor-keys": "8.53.0",
+ "debug": "^4.4.3",
+ "minimatch": "^9.0.5",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.0.tgz",
+ "integrity": "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.53.0",
+ "@typescript-eslint/types": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz",
+ "integrity": "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.53.0",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
+ "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.53",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.15",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.15.tgz",
+ "integrity": "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001764",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
+ "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
+ "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@epic-web/invariant": "^1.0.0",
+ "cross-spawn": "^7.0.6"
+ },
+ "bin": {
+ "cross-env": "dist/bin/cross-env.js",
+ "cross-env-shell": "dist/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/dompurify": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
+ "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/embla-carousel": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
+ "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
+ "license": "MIT"
+ },
+ "node_modules/embla-carousel-autoplay": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-autoplay/-/embla-carousel-autoplay-8.6.0.tgz",
+ "integrity": "sha512-OBu5G3nwaSXkZCo1A6LTaFMZ8EpkYbwIaH+bPqdBnDGQ2fh4+NbzjXjs2SktoPNKCtflfVMc75njaDHOYXcrsA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.6.0"
+ }
+ },
+ "node_modules/embla-carousel-fade": {
+ "version": "8.6.0",
+ "resolved": "https://registry.npmjs.org/embla-carousel-fade/-/embla-carousel-fade-8.6.0.tgz",
+ "integrity": "sha512-qaYsx5mwCz72ZrjlsXgs1nKejSrW+UhkbOMwLgfRT7w2LtdEB03nPRI06GHuHv5ac2USvbEiX2/nAHctcDwvpg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "embla-carousel": "8.6.0"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.2",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
+ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.1",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.39.2",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.4.26",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
+ "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": ">=8.40"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-nonce": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
+ "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
+ "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyborg": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.6.0.tgz",
+ "integrity": "sha512-o5kvLbuTF+o326CMVYpjlaykxqYP9DphFQZ2ZpgrvBouyvOxyEB7oqe8nOLFpiV5VCtz0D3pt8gXQYWpLpBnmA==",
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/marked": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
+ "license": "MIT",
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/monaco-editor": {
+ "version": "0.55.1",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
+ "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
+ "license": "MIT",
+ "dependencies": {
+ "dompurify": "3.2.7",
+ "marked": "14.0.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
+ "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.3"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "license": "MIT"
+ },
+ "node_modules/react-number-format": {
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.4.tgz",
+ "integrity": "sha512-wOmoNZoOpvMminhifQYiYSTCLUDOiUbBunrMrMjA+dV52sY+vck1S4UhR6PkgnoCquvvMSeJjErXZ4qSaWCliA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-remove-scroll": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
+ "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.7",
+ "react-style-singleton": "^2.2.3",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.3",
+ "use-sidecar": "^1.1.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
+ "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.2",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router": {
+ "version": "7.12.0",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
+ "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.12.0",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz",
+ "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.12.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/react-style-singleton": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
+ "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-textarea-autosize": {
+ "version": "8.5.9",
+ "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.9.tgz",
+ "integrity": "sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.20.13",
+ "use-composed-ref": "^1.3.0",
+ "use-latest": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
+ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.55.1",
+ "@rollup/rollup-android-arm64": "4.55.1",
+ "@rollup/rollup-darwin-arm64": "4.55.1",
+ "@rollup/rollup-darwin-x64": "4.55.1",
+ "@rollup/rollup-freebsd-arm64": "4.55.1",
+ "@rollup/rollup-freebsd-x64": "4.55.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.55.1",
+ "@rollup/rollup-linux-arm64-musl": "4.55.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.55.1",
+ "@rollup/rollup-linux-loong64-musl": "4.55.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.55.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.55.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-musl": "4.55.1",
+ "@rollup/rollup-openbsd-x64": "4.55.1",
+ "@rollup/rollup-openharmony-arm64": "4.55.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.55.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.55.1",
+ "@rollup/rollup-win32-x64-gnu": "4.55.1",
+ "@rollup/rollup-win32-x64-msvc": "4.55.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/rtl-css-js": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
+ "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.1.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/state-local": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
+ "license": "MIT"
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
+ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/tabbable": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
+ "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==",
+ "license": "MIT"
+ },
+ "node_modules/tabster": {
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/tabster/-/tabster-8.7.0.tgz",
+ "integrity": "sha512-AKYquti8AdWzuqJdQo4LUMQDZrHoYQy6V+8yUq2PmgLZV10EaB+8BD0nWOfC/3TBp4mPNg4fbHkz6SFtkr0PpA==",
+ "license": "MIT",
+ "dependencies": {
+ "keyborg": "2.6.0",
+ "tslib": "^2.8.1"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-linux-x64-gnu": "4.53.3"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
+ "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.53.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.0.tgz",
+ "integrity": "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.53.0",
+ "@typescript-eslint/parser": "8.53.0",
+ "@typescript-eslint/typescript-estree": "8.53.0",
+ "@typescript-eslint/utils": "8.53.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/use-callback-ref": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
+ "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-composed-ref": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.4.0.tgz",
+ "integrity": "sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-isomorphic-layout-effect": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
+ "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-latest": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.3.0.tgz",
+ "integrity": "sha512-mhg3xdm9NaM8q+gLT8KryJPnRFOz1/5XPBhmDEVZK1webPzDjrPk7f/mbpeLqTgB9msytYWANxgALOCJKnLvcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "use-isomorphic-layout-effect": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sidecar": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
+ "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
+ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/app/sb-explorer-ui/package.json b/app/sb-explorer-ui/package.json
new file mode 100644
index 0000000..4e8273d
--- /dev/null
+++ b/app/sb-explorer-ui/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "sb-explorer-ui",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "cross-env VITE_API_BASE_URL=http://localhost:5123/api VITE_USE_MOCK=false VITE_USE_MOCK_QUEUES=false VITE_USE_MOCK_TOPICS=false VITE_USE_MOCK_SUBSCRIPTIONS=false VITE_USE_MOCK_MESSAGES=false VITE_USE_MOCK_DLQ=false vite",
+ "dev:mock": "cross-env VITE_API_BASE_URL=/api VITE_USE_MOCK=false vite",
+ "dev:partial": "cross-env VITE_API_BASE_URL=http://localhost:5123/api VITE_USE_MOCK=false VITE_USE_MOCK_QUEUES=false VITE_USE_MOCK_TOPICS=false VITE_USE_MOCK_SUBSCRIPTIONS=false VITE_USE_MOCK_MESSAGES=false VITE_USE_MOCK_DLQ=true vite",
+ "build": "cross-env VITE_API_BASE_URL=/api VITE_USE_MOCK=false VITE_USE_MOCK_QUEUES=false VITE_USE_MOCK_TOPICS=false VITE_USE_MOCK_SUBSCRIPTIONS=false VITE_USE_MOCK_MESSAGES=false VITE_USE_MOCK_DLQ=false tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@fluentui/react-components": "^9.72.9",
+ "@fluentui/react-icons": "^2.0.316",
+ "@mantine/core": "^8.3.12",
+ "@mantine/hooks": "^8.3.12",
+ "@mantine/notifications": "^8.3.12",
+ "@monaco-editor/react": "^4.7.0",
+ "@tabler/icons-react": "^3.36.1",
+ "@tanstack/react-query": "^5.90.18",
+ "@tanstack/react-query-devtools": "^5.91.2",
+ "axios": "^1.13.2",
+ "monaco-editor": "^0.55.1",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.12.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.1",
+ "@types/node": "^24.10.1",
+ "@types/react": "^19.2.5",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react": "^5.1.1",
+ "cross-env": "^10.1.0",
+ "eslint": "^9.39.1",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.4.24",
+ "globals": "^16.5.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.46.4",
+ "vite": "^7.2.4"
+ }
+}
diff --git a/app/sb-explorer-ui/public/servicebus.png b/app/sb-explorer-ui/public/servicebus.png
new file mode 100644
index 0000000..83a619a
Binary files /dev/null and b/app/sb-explorer-ui/public/servicebus.png differ
diff --git a/app/sb-explorer-ui/public/vite.svg b/app/sb-explorer-ui/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/app/sb-explorer-ui/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/sb-explorer-ui/src/App.css b/app/sb-explorer-ui/src/App.css
new file mode 100644
index 0000000..b9d355d
--- /dev/null
+++ b/app/sb-explorer-ui/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: 2rem;
+ text-align: center;
+}
+
+.logo {
+ height: 6em;
+ padding: 1.5em;
+ will-change: filter;
+ transition: filter 300ms;
+}
+.logo:hover {
+ filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+ filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+ a:nth-of-type(2) .logo {
+ animation: logo-spin infinite 20s linear;
+ }
+}
+
+.card {
+ padding: 2em;
+}
+
+.read-the-docs {
+ color: #888;
+}
diff --git a/app/sb-explorer-ui/src/App.tsx b/app/sb-explorer-ui/src/App.tsx
new file mode 100644
index 0000000..3249674
--- /dev/null
+++ b/app/sb-explorer-ui/src/App.tsx
@@ -0,0 +1,65 @@
+import { createContext, useContext, useEffect, useMemo, useState } from 'react'
+import { MantineProvider } from '@mantine/core'
+import { Notifications } from '@mantine/notifications'
+import { Navigate, RouterProvider, createBrowserRouter } from 'react-router-dom'
+import AppShell from './routes/AppShell'
+import Queues from './routes/Queues'
+import QueueDetail from './routes/QueueDetail'
+import Topics from './routes/Topics'
+import TopicDetail from './routes/TopicDetail'
+import AppError from './components/AppError'
+import { mantineLight, mantineDark } from './theme'
+
+type ColorScheme = 'light' | 'dark'
+
+interface AppContextValue {
+ theme: ColorScheme
+ toggleTheme: () => void
+}
+
+const AppContext = createContext(undefined)
+
+export const useAppContext = () => {
+ const value = useContext(AppContext)
+ if (!value) throw new Error('useAppContext must be used within App')
+ return value
+}
+
+function App() {
+ const [theme, setTheme] = useState(() => (localStorage.getItem('sbx-theme') as ColorScheme) || 'light')
+ const toggleTheme = () => setTheme((prev: ColorScheme) => (prev === 'light' ? 'dark' : 'light'))
+
+ useEffect(() => {
+ localStorage.setItem('sbx-theme', theme)
+ }, [theme])
+
+ const router = useMemo(
+ () =>
+ createBrowserRouter([
+ {
+ element: ,
+ errorElement: ,
+ children: [
+ { index: true, element: },
+ { path: '/queues', element: , errorElement: },
+ { path: '/queues/:name', element: , errorElement: },
+ { path: '/topics', element: , errorElement: },
+ { path: '/topics/:name', element: , errorElement: },
+ { path: '/topics/:name/:subscription', element: , errorElement: },
+ ],
+ },
+ ]),
+ [],
+ )
+
+ return (
+
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/app/sb-explorer-ui/src/api/client.ts b/app/sb-explorer-ui/src/api/client.ts
new file mode 100644
index 0000000..1bb7676
--- /dev/null
+++ b/app/sb-explorer-ui/src/api/client.ts
@@ -0,0 +1,346 @@
+import axios, { type AxiosAdapter, type AxiosRequestConfig } from "axios";
+import type {
+ MessageInfo,
+ MessageScope,
+ MessageState,
+ PagedResult,
+ QueueInfo,
+ SendScope,
+ SubscriptionInfo,
+ TopicInfo,
+} from "./types";
+
+const baseURL = import.meta.env.VITE_API_BASE_URL ?? "/api";
+
+const flag = (value: unknown) =>
+ value === true || value === "true" || value === "1";
+const mockFlag = (value: unknown, defaultVal: boolean) =>
+ value === undefined ? defaultVal : flag(value);
+
+const useMockAll = flag(import.meta.env.VITE_USE_MOCK);
+const useMockQueues =
+ useMockAll || mockFlag(import.meta.env.VITE_USE_MOCK_QUEUES, false);
+const useMockTopics =
+ useMockAll || mockFlag(import.meta.env.VITE_USE_MOCK_TOPICS, false);
+const useMockSubs =
+ useMockAll || mockFlag(import.meta.env.VITE_USE_MOCK_SUBSCRIPTIONS, false);
+const useMockMessages =
+ useMockAll || mockFlag(import.meta.env.VITE_USE_MOCK_MESSAGES, false);
+const useMockDlq =
+ useMockAll || mockFlag(import.meta.env.VITE_USE_MOCK_DLQ, false);
+const anyMockEnabled =
+ useMockQueues ||
+ useMockTopics ||
+ useMockSubs ||
+ useMockMessages ||
+ useMockDlq;
+
+export const apiBaseUrl = baseURL;
+export const isMockEnabled = anyMockEnabled;
+export const mockMatrix = {
+ queues: useMockQueues,
+ topics: useMockTopics,
+ subs: useMockSubs,
+ messages: useMockMessages,
+ dlq: useMockDlq,
+};
+
+console.log("[SB-Explorer] Configuration:", {
+ apiBaseUrl,
+ isMockEnabled,
+ mockMatrix,
+ env: import.meta.env,
+});
+
+export const messagePath = (scope: SendScope) => {
+ if (scope.type === "queue") return `/queues/${scope.name}/messages`;
+ if (scope.type === "topic") return `/topics/${scope.name}/messages`;
+ return `/topics/${scope.topic}/subscriptions/${scope.subscription}/messages`;
+};
+
+export const dlqPath = (scope: MessageScope) => {
+ if (scope.type === "queue") return `/deadletter/queue/${scope.name}/delete`;
+ return `/deadletter/subscription/${scope.topic}/${scope.subscription}/delete`;
+};
+
+const respond = (data: T, config: AxiosRequestConfig) =>
+ Promise.resolve({
+ data,
+ status: 200,
+ statusText: "OK",
+ headers: config.headers ?? {},
+ config,
+ request: {},
+ } as any);
+
+// no explicit notFound; fall through to real adapter when not mocking
+
+const mockData = (() => {
+ const queues: QueueInfo[] = [
+ {
+ name: "orders",
+ status: "Active",
+ activeMessageCount: 2,
+ deadLetterMessageCount: 1,
+ maxDeliveryCount: 10,
+ lockDuration: "00:01:00",
+ defaultTtl: "1.00:00:00",
+ createdAt: new Date().toISOString(),
+ },
+ {
+ name: "payments",
+ status: "Active",
+ activeMessageCount: 0,
+ deadLetterMessageCount: 0,
+ maxDeliveryCount: 5,
+ lockDuration: "00:00:45",
+ defaultTtl: "1.00:00:00",
+ createdAt: new Date().toISOString(),
+ },
+ ];
+
+ const topics: TopicInfo[] = [
+ {
+ name: "invoices",
+ status: "Active",
+ activeMessageCount: 3,
+ deadLetterMessageCount: 0,
+ createdAt: new Date().toISOString(),
+ },
+ ];
+
+ const subscriptions: Record = {
+ invoices: [
+ {
+ name: "processor",
+ status: "Active",
+ activeMessageCount: 2,
+ deadLetterMessageCount: 0,
+ maxDeliveryCount: 10,
+ lockDuration: "00:01:00",
+ defaultTtl: "1.00:00:00",
+ },
+ {
+ name: "archiver",
+ status: "Active",
+ activeMessageCount: 1,
+ deadLetterMessageCount: 0,
+ maxDeliveryCount: 5,
+ lockDuration: "00:00:45",
+ defaultTtl: "1.00:00:00",
+ },
+ ],
+ };
+
+ const messages: Record = {
+ "queue:orders:active": [
+ {
+ messageId: "m1",
+ bodyPreview: "Order 123",
+ body: JSON.stringify({ id: 123, status: "new" }, null, 2),
+ enqueuedTime: new Date().toISOString(),
+ deliveryCount: 1,
+ contentType: "application/json",
+ },
+ {
+ messageId: "m2",
+ bodyPreview: "Order 124",
+ body: JSON.stringify({ id: 124, status: "new" }, null, 2),
+ enqueuedTime: new Date().toISOString(),
+ deliveryCount: 1,
+ contentType: "application/json",
+ },
+ ],
+ "queue:orders:deadletter": [
+ {
+ messageId: "dlq-1",
+ bodyPreview: "Poison message",
+ body: "bad payload",
+ deliveryCount: 5,
+ },
+ {
+ messageId: "dlq-2",
+ bodyPreview: "Another poison message",
+ body: "very bad payload",
+ deliveryCount: 7,
+ },
+ ],
+ "subscription:invoices:processor:active": [
+ {
+ messageId: "s1",
+ bodyPreview: "Invoice created",
+ body: JSON.stringify({ invoiceId: "inv-1" }, null, 2),
+ enqueuedTime: new Date().toISOString(),
+ },
+ ],
+ "subscription:invoices:archiver:active": [
+ {
+ messageId: "s2",
+ bodyPreview: "Invoice archived",
+ body: JSON.stringify({ invoiceId: "inv-2" }, null, 2),
+ enqueuedTime: new Date().toISOString(),
+ },
+ ],
+ };
+
+ const paginate = (
+ key: string,
+ _state: MessageState,
+ skip = 0,
+ take = 25,
+ ): PagedResult => {
+ const items = messages[key] ?? [];
+ const slice = items.slice(skip, skip + take);
+ return {
+ items: slice,
+ total: items.length,
+ hasMore: skip + take < items.length,
+ };
+ };
+
+ return { queues, topics, subscriptions, messages, paginate };
+})();
+
+const mockAdapter: AxiosAdapter = async (config) => {
+ const { method = "get", url, data, params } = config;
+ const path = url?.replace(baseURL, "") ?? "";
+
+ // Queues
+ if (useMockQueues) {
+ if (method === "get" && path === "/queues")
+ return respond(mockData.queues, config);
+ if (method === "post" && path === "/queues") {
+ const payload = typeof data === "string" ? JSON.parse(data) : data;
+ const exists = mockData.queues.find((q) => q.name === payload.name);
+ if (!exists) {
+ mockData.queues.push({
+ name: payload.name,
+ status: "Active",
+ activeMessageCount: 0,
+ deadLetterMessageCount: 0,
+ maxDeliveryCount: payload.maxDeliveryCount ?? 10,
+ lockDuration: payload.lockDuration ?? "00:01:00",
+ defaultTtl: payload.defaultTtl ?? "1.00:00:00",
+ createdAt: new Date().toISOString(),
+ });
+ }
+ return respond({}, config);
+ }
+ if (method === "delete" && path?.startsWith("/queues/")) {
+ const name = path.split("/")[2];
+ const idx = mockData.queues.findIndex((q) => q.name === name);
+ if (idx >= 0) mockData.queues.splice(idx, 1);
+ return respond({}, config);
+ }
+ }
+
+ // Topics
+ if (useMockTopics) {
+ if (method === "get" && path === "/topics")
+ return respond(mockData.topics, config);
+ if (method === "post" && path === "/topics") {
+ const payload = typeof data === "string" ? JSON.parse(data) : data;
+ const exists = mockData.topics.find((t) => t.name === payload.name);
+ if (!exists) {
+ mockData.topics.push({
+ name: payload.name,
+ status: "Active",
+ activeMessageCount: 0,
+ deadLetterMessageCount: 0,
+ createdAt: new Date().toISOString(),
+ });
+ }
+ return respond({}, config);
+ }
+ if (
+ method === "delete" &&
+ path?.startsWith("/topics/") &&
+ !path.includes("/subscriptions")
+ ) {
+ const name = path.split("/")[2];
+ const idx = mockData.topics.findIndex((t) => t.name === name);
+ if (idx >= 0) mockData.topics.splice(idx, 1);
+ delete mockData.subscriptions[name];
+ return respond({}, config);
+ }
+
+ if (useMockSubs) {
+ // Subscriptions
+ if (
+ method === "get" &&
+ path?.startsWith("/topics/") &&
+ path.endsWith("/subscriptions")
+ ) {
+ const topic = path.split("/")[2];
+ return respond(mockData.subscriptions[topic] ?? [], config);
+ }
+ if (
+ method === "post" &&
+ path?.startsWith("/topics/") &&
+ path.endsWith("/subscriptions")
+ ) {
+ const topic = path.split("/")[2];
+ const payload = typeof data === "string" ? JSON.parse(data) : data;
+ const list = mockData.subscriptions[topic] ?? [];
+ const exists = list.find((s) => s.name === payload.name);
+ if (!exists) {
+ list.push({
+ name: payload.name,
+ status: "Active",
+ activeMessageCount: 0,
+ deadLetterMessageCount: 0,
+ maxDeliveryCount: payload.maxDeliveryCount ?? 10,
+ lockDuration: payload.lockDuration ?? "00:01:00",
+ defaultTtl: payload.defaultTtl ?? "1.00:00:00",
+ });
+ mockData.subscriptions[topic] = list;
+ }
+ return respond({}, config);
+ }
+ if (method === "delete" && path?.includes("/subscriptions/")) {
+ const [, , topic, , subName] = path.split("/");
+ const list = mockData.subscriptions[topic] ?? [];
+ const idx = list.findIndex((s) => s.name === subName);
+ if (idx >= 0) list.splice(idx, 1);
+ mockData.subscriptions[topic] = list;
+ return respond({}, config);
+ }
+ }
+ }
+
+ // Messages
+ if (useMockMessages && method === "get" && path?.includes("/messages")) {
+ const state = (params?.state as MessageState) ?? "active";
+ const skip = Number(params?.skip ?? 0);
+ const take = Number(params?.take ?? 25);
+ if (path.startsWith("/queues/")) {
+ const [, , queueName] = path.split("/");
+ const key = `queue:${queueName}:${state}`;
+ return respond(mockData.paginate(key, state, skip, take), config);
+ }
+ if (path.startsWith("/topics/")) {
+ const [, , topic, , subName] = path.split("/");
+ const key = `subscription:${topic}:${subName}:${state}`;
+ return respond(mockData.paginate(key, state, skip, take), config);
+ }
+ }
+
+ if (useMockMessages && method === "post" && path?.includes("/messages")) {
+ // treat as send; just ack
+ return respond({}, config);
+ }
+
+ if (useMockDlq && method === "post" && path?.includes("/deadletter/")) {
+ // bulk delete DLQ
+ return respond({}, config);
+ }
+
+ const { adapter, ...configWithoutAdapter } = config;
+ return axios.request(configWithoutAdapter);
+};
+
+export const apiClient = axios.create({
+ baseURL,
+ timeout: 12000,
+ adapter: anyMockEnabled ? (config) => mockAdapter(config as any) : undefined,
+});
diff --git a/app/sb-explorer-ui/src/api/hooks.ts b/app/sb-explorer-ui/src/api/hooks.ts
new file mode 100644
index 0000000..3e0737a
--- /dev/null
+++ b/app/sb-explorer-ui/src/api/hooks.ts
@@ -0,0 +1,165 @@
+import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { apiClient, dlqPath, messagePath } from './client'
+import type {
+ MessageInfo,
+ MessageScope,
+ MessageState,
+ PagedResult,
+ QueueInfo,
+ SubscriptionInfo,
+ TopicInfo,
+} from './types'
+
+const listRefetchMs = 8000
+
+const scopeKey = (scope: MessageScope) =>
+ scope.type === 'queue' ? `queue:${scope.name}` : `subscription:${scope.topic}:${scope.subscription}`
+
+export const useQueues = () =>
+ useQuery({
+ queryKey: ['queues'],
+ queryFn: async () => (await apiClient.get('/queues')).data,
+ refetchInterval: listRefetchMs,
+ })
+
+export const useTopics = () =>
+ useQuery({
+ queryKey: ['topics'],
+ queryFn: async () => (await apiClient.get('/topics')).data,
+ refetchInterval: listRefetchMs,
+ })
+
+export const useSubscriptions = (topic: string, enabled = true) =>
+ useQuery({
+ queryKey: ['subs', topic],
+ queryFn: async () => (await apiClient.get(`/topics/${topic}/subscriptions`)).data,
+ enabled: !!topic && enabled,
+ refetchInterval: listRefetchMs,
+ })
+
+interface MessagesQuery {
+ scope: MessageScope
+ state: MessageState
+ skip: number
+ take: number
+ enabled?: boolean
+}
+
+const isScopeValid = (scope: MessageScope) => {
+ if (scope.type === 'queue') return !!scope.name
+ return !!scope.topic && !!scope.subscription
+}
+
+export const useMessages = ({ scope, state, skip, take, enabled = true }: MessagesQuery) =>
+ useQuery({
+ queryKey: ['messages', scopeKey(scope), state, skip, take],
+ queryFn: async () =>
+ (
+ await apiClient.get>(messagePath(scope), {
+ params: { mode: 'peek', state, skip, take },
+ })
+ ).data,
+ enabled: enabled && isScopeValid(scope),
+ placeholderData: keepPreviousData,
+ })
+
+export const useCreateQueue = () => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async (payload: Partial & { name: string }) => {
+ await apiClient.post('/queues', payload)
+ },
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['queues'] }),
+ })
+}
+
+export const useCreateTopic = () => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async (payload: Partial & { name: string }) => {
+ await apiClient.post('/topics', payload)
+ },
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['topics'] }),
+ })
+}
+
+export const useCreateSubscription = (topic: string) => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async (payload: Partial & { name: string }) => {
+ await apiClient.post(`/topics/${topic}/subscriptions`, payload)
+ },
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['subs', topic] }),
+ })
+}
+
+export const useDeleteQueue = () => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async (name: string) => {
+ await apiClient.delete(`/queues/${name}`)
+ },
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['queues'] }),
+ })
+}
+
+export const useDeleteTopic = () => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async (name: string) => {
+ await apiClient.delete(`/topics/${name}`)
+ },
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['topics'] }),
+ })
+}
+
+export const useDeleteSubscription = (topic: string) => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async (subName: string) => {
+ await apiClient.delete(`/topics/${topic}/subscriptions/${subName}`)
+ },
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['subs', topic] }),
+ })
+}
+
+interface SendMessageInput {
+ scope: MessageScope | { type: 'topic'; name: string }
+ body: string
+ contentType?: string
+ userProperties?: Record
+}
+
+export const useSendMessage = () => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async ({ scope, ...payload }: SendMessageInput) => {
+ await apiClient.post(messagePath(scope), payload)
+ },
+ onSuccess: (_data, variables) => {
+ if (variables.scope.type === 'queue' || variables.scope.type === 'subscription') {
+ qc.invalidateQueries({ queryKey: ['messages', scopeKey(variables.scope)] })
+ }
+ // Refresh lists to reflect new counts
+ qc.invalidateQueries({ queryKey: ['queues'] })
+ qc.invalidateQueries({ queryKey: ['topics'] })
+ },
+ })
+}
+
+interface BulkDlqDeleteInput {
+ scope: MessageScope
+ messageIds?: string[]
+}
+
+export const useBulkDlqDelete = () => {
+ const qc = useQueryClient()
+ return useMutation({
+ mutationFn: async ({ scope, messageIds }: BulkDlqDeleteInput) => {
+ await apiClient.post(dlqPath(scope), { messageIds })
+ },
+ onSuccess: (_data, variables) => {
+ qc.invalidateQueries({ queryKey: ['messages', scopeKey(variables.scope)] })
+ },
+ })
+}
diff --git a/app/sb-explorer-ui/src/api/types.ts b/app/sb-explorer-ui/src/api/types.ts
new file mode 100644
index 0000000..f3a8eec
--- /dev/null
+++ b/app/sb-explorer-ui/src/api/types.ts
@@ -0,0 +1,60 @@
+export type EntityStatus = 'Active' | 'Disabled' | 'SendDisabled' | 'ReceiveDisabled'
+
+export interface QueueInfo {
+ name: string
+ status: EntityStatus
+ activeMessageCount: number
+ deadLetterMessageCount: number
+ scheduledMessageCount?: number
+ createdAt?: string
+ maxDeliveryCount?: number
+ lockDuration?: string
+ defaultTtl?: string
+}
+
+export interface TopicInfo {
+ name: string
+ status: EntityStatus
+ activeMessageCount: number
+ deadLetterMessageCount: number
+ scheduledMessageCount?: number
+ createdAt?: string
+}
+
+export interface SubscriptionInfo {
+ name: string
+ status: EntityStatus
+ activeMessageCount: number
+ deadLetterMessageCount: number
+ scheduledMessageCount?: number
+ maxDeliveryCount?: number
+ lockDuration?: string
+ defaultTtl?: string
+}
+
+export interface MessageInfo {
+ messageId: string
+ bodyPreview: string
+ body?: string
+ enqueuedTime?: string
+ expiresAt?: string
+ deliveryCount?: number
+ contentType?: string
+ sessionId?: string
+ userProperties?: Record
+ systemProperties?: Record
+}
+
+export interface PagedResult {
+ items: T[]
+ total?: number
+ hasMore?: boolean
+}
+
+export type MessageState = 'active' | 'deadletter'
+
+export type MessageScope =
+ | { type: 'queue'; name: string }
+ | { type: 'subscription'; topic: string; subscription: string }
+
+export type SendScope = MessageScope | { type: 'topic'; name: string }
diff --git a/app/sb-explorer-ui/src/assets/react.svg b/app/sb-explorer-ui/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/app/sb-explorer-ui/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/sb-explorer-ui/src/components/AppError.tsx b/app/sb-explorer-ui/src/components/AppError.tsx
new file mode 100644
index 0000000..106f278
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/AppError.tsx
@@ -0,0 +1,21 @@
+import { Button, Card, Stack, Text, Title } from '@mantine/core'
+import { useNavigate, useRouteError } from 'react-router-dom'
+
+const AppError = () => {
+ const error = useRouteError() as any
+ const navigate = useNavigate()
+
+ return (
+
+
+ Something went wrong
+
+ {error?.statusText || error?.message || 'Unexpected application error.'}
+
+ navigate('/')}>Return home
+
+
+ )
+}
+
+export default AppError
diff --git a/app/sb-explorer-ui/src/components/EntityHeader.tsx b/app/sb-explorer-ui/src/components/EntityHeader.tsx
new file mode 100644
index 0000000..f481a0e
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/EntityHeader.tsx
@@ -0,0 +1,78 @@
+import { ActionIcon, Badge, Group, Paper, Title, Tooltip } from '@mantine/core'
+import { IconPlus, IconSend, IconTrash } from '@tabler/icons-react'
+import type { EntityStatus } from '../api/types'
+import StatusPill from './StatusPill'
+
+interface EntityHeaderProps {
+ name: string
+ type: 'queue' | 'topic' | 'subscription'
+ status: EntityStatus
+ activeCount?: number
+ deadLetterCount?: number
+ onSend?: () => void
+ onDelete?: () => void
+ onCreateSubscription?: () => void
+}
+
+const EntityHeader = ({
+ name,
+ type,
+ status,
+ activeCount,
+ deadLetterCount,
+ onSend,
+ onDelete,
+ onCreateSubscription,
+}: EntityHeaderProps) => {
+ return (
+
+
+
+ {name}
+
+
+ {type}
+
+
+ {activeCount !== undefined && (
+
+ Active: {activeCount}
+
+ )}
+ {deadLetterCount !== undefined && (
+
+ DLQ: {deadLetterCount}
+
+ )}
+
+
+
+
+ {type !== 'subscription' && onCreateSubscription && (
+
+
+
+
+
+ )}
+ {onSend && (
+
+
+
+
+
+ )}
+ {onDelete && (
+
+
+
+
+
+ )}
+
+
+
+ )
+}
+
+export default EntityHeader
diff --git a/app/sb-explorer-ui/src/components/EntityOverviewCard.tsx b/app/sb-explorer-ui/src/components/EntityOverviewCard.tsx
new file mode 100644
index 0000000..4dd89f3
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/EntityOverviewCard.tsx
@@ -0,0 +1,28 @@
+import { Card, Group, SimpleGrid, Text } from '@mantine/core'
+
+interface OverviewCardProps {
+ title: string
+ items: { label: string; value?: string | number | null }[]
+}
+
+const EntityOverviewCard = ({ title, items }: OverviewCardProps) => {
+ return (
+
+
+ {title}
+
+
+ {items.map((item) => (
+
+
+ {item.label}
+
+ {item.value ?? '—'}
+
+ ))}
+
+
+ )
+}
+
+export default EntityOverviewCard
diff --git a/app/sb-explorer-ui/src/components/EntityTable.tsx b/app/sb-explorer-ui/src/components/EntityTable.tsx
new file mode 100644
index 0000000..5396880
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/EntityTable.tsx
@@ -0,0 +1,139 @@
+import type { ReactNode } from 'react'
+import { ActionIcon, Button, Group, Paper, ScrollArea, Table, Text, Tooltip } from '@mantine/core'
+import { IconPlus, IconRefresh } from '@tabler/icons-react'
+import type { EntityStatus } from '../api/types'
+import StatusPill from './StatusPill'
+
+export interface EntityRow {
+ name: string
+ status: EntityStatus
+ activeMessageCount: number
+ deadLetterMessageCount: number
+ scheduledMessageCount?: number
+ createdAt?: string
+}
+
+interface EntityTableProps {
+ title: string
+ items?: T[]
+ loading?: boolean
+ onRefresh?: () => void
+ onCreate?: () => void
+ onRowClick?: (item: T) => void
+ emptyState?: ReactNode
+}
+
+type Column = {
+ id: string
+ label: string
+ render: (item: T) => ReactNode
+}
+
+const columns: Column[] = [
+ {
+ id: 'name',
+ label: 'Name',
+ render: (item) => {item.name} ,
+ },
+ {
+ id: 'status',
+ label: 'Status',
+ render: (item) => ,
+ },
+ {
+ id: 'active',
+ label: 'Active',
+ render: (item) => item.activeMessageCount ?? 0,
+ },
+ {
+ id: 'dlq',
+ label: 'Dead-letter',
+ render: (item) => item.deadLetterMessageCount ?? 0,
+ },
+ {
+ id: 'scheduled',
+ label: 'Scheduled',
+ render: (item) => item.scheduledMessageCount ?? 0,
+ },
+ {
+ id: 'created',
+ label: 'Created',
+ render: (item) => (item.createdAt ? new Date(item.createdAt).toLocaleString() : '—'),
+ },
+]
+
+const EntityTable = ({
+ title,
+ items,
+ loading,
+ onCreate,
+ onRefresh,
+ onRowClick,
+ emptyState,
+}: EntityTableProps) => {
+ const list: T[] = Array.isArray(items) ? items : items ? Object.values(items as any) : []
+
+ return (
+
+
+
+
+
+
+
+
+ {title}
+
+ {onCreate && (
+ } onClick={onCreate} variant="light">
+ Create
+
+ )}
+
+
+
+
+
+
+ {columns.map((column) => (
+ {column.label}
+ ))}
+
+
+
+ {list.map((item) => (
+ onRowClick?.(item)}
+ style={{ cursor: onRowClick ? 'pointer' : 'default' }}
+ >
+ {columns.map((column) => (
+ {column.render(item)}
+ ))}
+
+ ))}
+
+
+
+
+ {!loading && list.length === 0 && emptyState && {emptyState}
}
+
+ )
+}
+
+export default EntityTable
diff --git a/app/sb-explorer-ui/src/components/MessageDetailPanel.tsx b/app/sb-explorer-ui/src/components/MessageDetailPanel.tsx
new file mode 100644
index 0000000..6adaaa9
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/MessageDetailPanel.tsx
@@ -0,0 +1,44 @@
+import { useMemo } from 'react'
+import { Divider, Stack, Text } from '@mantine/core'
+import Editor from '@monaco-editor/react'
+import type { MessageInfo } from '../api/types'
+import MessagePropertiesTable from './MessagePropertiesTable'
+import { useAppContext } from '../App'
+import SideDrawer from './SideDrawer'
+
+interface MessageDetailPanelProps {
+ message?: MessageInfo
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+const MessageDetailPanel = ({ message, open, onOpenChange }: MessageDetailPanelProps) => {
+ const { theme } = useAppContext()
+ const bodyValue = useMemo(() => message?.body ?? message?.bodyPreview ?? '', [message])
+ const monacoTheme = theme === 'dark' ? 'vs-dark' : 'vs'
+
+ return (
+
+
+ Content Type: {message?.contentType ?? '—'}
+ Enqueued: {message?.enqueuedTime ? new Date(message.enqueuedTime).toLocaleString() : '—'}
+ Delivery count: {message?.deliveryCount ?? 0}
+
+
+
+
+
+
+ | undefined} />
+ | undefined} />
+
+ )
+}
+
+export default MessageDetailPanel
diff --git a/app/sb-explorer-ui/src/components/MessageGrid.tsx b/app/sb-explorer-ui/src/components/MessageGrid.tsx
new file mode 100644
index 0000000..1ca9e00
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/MessageGrid.tsx
@@ -0,0 +1,155 @@
+import { useMemo } from 'react'
+import type { ReactNode } from 'react'
+import { ActionIcon, Checkbox, Group, Paper, ScrollArea, Table, Text, Tooltip } from '@mantine/core'
+import { IconArrowLeft, IconArrowRight } from '@tabler/icons-react'
+import type { MessageInfo, MessageState, PagedResult } from '../api/types'
+
+interface MessageGridProps {
+ messages?: PagedResult
+ loading?: boolean
+ state: MessageState
+ skip: number
+ take: number
+ onPageChange: (nextSkip: number) => void
+ selectedIds: string[]
+ onSelectionChange: (ids: string[]) => void
+ onInspect?: (message: MessageInfo) => void
+}
+
+type Column = {
+ id: string
+ label: string
+ render: (item: T) => ReactNode
+}
+
+const columns: Column[] = [
+ {
+ id: 'subject',
+ label: 'Body',
+ render: (item) => item.bodyPreview,
+ },
+ {
+ id: 'messageId',
+ label: 'Message ID',
+ render: (item) => item.messageId,
+ },
+ {
+ id: 'enqueued',
+ label: 'Enqueued',
+ render: (item) => (item.enqueuedTime ? new Date(item.enqueuedTime).toLocaleString() : '—'),
+ },
+ {
+ id: 'delivery',
+ label: 'Delivery count',
+ render: (item) => item.deliveryCount ?? 0,
+ },
+ {
+ id: 'session',
+ label: 'Session',
+ render: (item) => item.sessionId ?? '—',
+ },
+]
+
+const MessageGrid = ({
+ messages,
+ loading,
+ state: _state,
+ skip,
+ take,
+ onPageChange,
+ selectedIds,
+ onSelectionChange,
+ onInspect,
+}: MessageGridProps) => {
+ const items = messages?.items ?? []
+ const hasNext = messages?.hasMore || (messages?.total ? skip + take < messages.total : items.length === take)
+ const canPrev = skip > 0
+
+ const toggleSelection = (id: string) => {
+ if (selectedIds.includes(id)) onSelectionChange(selectedIds.filter((x) => x !== id))
+ else onSelectionChange([...selectedIds, id])
+ }
+
+ const allSelected = useMemo(() => items.length > 0 && items.every((m) => selectedIds.includes(m.messageId)), [items, selectedIds])
+
+ const toggleSelectAll = () => {
+ if (allSelected) onSelectionChange([])
+ else onSelectionChange(items.map((m) => m.messageId))
+ }
+
+ const start = items.length ? skip + 1 : 0
+ const end = skip + items.length
+
+ return (
+
+
+
+ onPageChange(Math.max(0, skip - take))}
+ >
+
+
+
+
+ onPageChange(skip + take)}
+ >
+
+
+
+
+ Showing {start} - {end}
+
+
+
+
+
+
+
+
+ toggleSelectAll()} />
+
+ {columns.map((column) => (
+ {column.label}
+ ))}
+
+
+
+ {items.map((item) => (
+ onInspect?.(item)}>
+
+ e.stopPropagation()}
+ onChange={() => toggleSelection(item.messageId)}
+ />
+
+ {columns.map((column) => (
+ {column.render(item)}
+ ))}
+
+ ))}
+
+
+
+
+ )
+}
+
+export default MessageGrid
diff --git a/app/sb-explorer-ui/src/components/MessagePropertiesTable.tsx b/app/sb-explorer-ui/src/components/MessagePropertiesTable.tsx
new file mode 100644
index 0000000..368cff8
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/MessagePropertiesTable.tsx
@@ -0,0 +1,53 @@
+import { Card, Divider, Group, ScrollArea, Table, Text } from '@mantine/core'
+
+interface PropsTableProps {
+ title: string
+ data?: Record
+}
+
+const MessagePropertiesTable = ({ title, data }: PropsTableProps) => {
+ const entries = data ? Object.entries(data) : []
+
+ return (
+
+
+ {title}
+
+
+
+
+
+
+ Key
+ Value
+
+
+
+ {entries.length === 0 ? (
+
+
+
+ None
+
+
+
+ ) : (
+ entries.map(([k, v]) => (
+
+
+ {k}
+
+
+ {typeof v === 'object' ? JSON.stringify(v) : String(v)}
+
+
+ ))
+ )}
+
+
+
+
+ )
+}
+
+export default MessagePropertiesTable
diff --git a/app/sb-explorer-ui/src/components/MessageTabs.tsx b/app/sb-explorer-ui/src/components/MessageTabs.tsx
new file mode 100644
index 0000000..6803065
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/MessageTabs.tsx
@@ -0,0 +1,22 @@
+import { Tabs } from '@mantine/core'
+import type { MessageState } from '../api/types'
+
+interface MessageTabsProps {
+ state: MessageState
+ onChange: (next: MessageState) => void
+ activeCount?: number
+ deadLetterCount?: number
+}
+
+const MessageTabs = ({ state, onChange, activeCount, deadLetterCount }: MessageTabsProps) => {
+ return (
+ onChange((value as MessageState) ?? 'active')} variant="pills" radius="md">
+
+ Active ({activeCount ?? 0})
+ Dead-letter ({deadLetterCount ?? 0})
+
+
+ )
+}
+
+export default MessageTabs
diff --git a/app/sb-explorer-ui/src/components/MockBanner.tsx b/app/sb-explorer-ui/src/components/MockBanner.tsx
new file mode 100644
index 0000000..6e2696b
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/MockBanner.tsx
@@ -0,0 +1,27 @@
+import { Alert, Group, Text } from '@mantine/core'
+import { apiBaseUrl, isMockEnabled, mockMatrix } from '../api/client'
+
+const MockBanner = () => {
+ if (!isMockEnabled) return null
+ const mocked = Object.entries(mockMatrix)
+ .filter(([, v]) => v)
+ .map(([k]) => k)
+ const live = Object.entries(mockMatrix)
+ .filter(([, v]) => !v)
+ .map(([k]) => k)
+ return (
+
+
+ Mock mode is ON.
+
+ Base URL: {apiBaseUrl}
+
+
+
+ Mocked: {mocked.join(', ') || 'none'} | Live: {live.join(', ') || 'none'}
+
+
+ )
+}
+
+export default MockBanner
diff --git a/app/sb-explorer-ui/src/components/NavTree.tsx b/app/sb-explorer-ui/src/components/NavTree.tsx
new file mode 100644
index 0000000..3b75a49
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/NavTree.tsx
@@ -0,0 +1,182 @@
+import { useMemo, useState, type MouseEvent } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { Badge, Box, Loader, NavLink, Stack } from '@mantine/core'
+import { IconChevronDown, IconChevronRight, IconMessageDots, IconTopologyStar, IconTopologyStar3 } from '@tabler/icons-react'
+import { useQueues, useSubscriptions, useTopics } from '../api/hooks'
+import type { TopicInfo } from '../api/types'
+
+const TopicBranch = ({
+ topic,
+ compact,
+ isOpen,
+ onToggle,
+}: {
+ topic: TopicInfo
+ compact: boolean
+ isOpen: boolean
+ onToggle: () => void
+}) => {
+ const navigate = useNavigate()
+ const { data: subs, isLoading } = useSubscriptions(topic.name, isOpen)
+ const handleToggleClick = (event: MouseEvent) => {
+ event.preventDefault()
+ event.stopPropagation()
+ onToggle()
+ }
+
+ return (
+ }
+ rightSection={
+
+ {isOpen ? : }
+
+ }
+ opened={isOpen}
+ onClick={() => navigate(`/topics/${topic.name}`)}
+ childrenOffset={compact ? 12 : 16}
+ >
+
+
+
+ {isOpen && isLoading && }
+ {isOpen &&
+ subs?.map((sub) => (
+ }
+ rightSection={sub.deadLetterMessageCount > 0 ? (
+
+ {sub.deadLetterMessageCount}
+
+ ) : undefined}
+ onClick={(e) => {
+ e.stopPropagation()
+ navigate(`/topics/${topic.name}/${sub.name}`)
+ }}
+ />
+ ))}
+
+ )
+}
+
+const NavTree = ({ compact = false }: { compact?: boolean }) => {
+ const navigate = useNavigate()
+ const { data: queues, isLoading: queuesLoading } = useQueues()
+ const { data: topics, isLoading: topicsLoading } = useTopics()
+
+ const queueList = useMemo(() => (Array.isArray(queues) ? queues : queues ? Object.values(queues as any) : []), [queues])
+ const topicList = useMemo(() => (Array.isArray(topics) ? topics : topics ? Object.values(topics as any) : []), [topics])
+
+ const [openItems, setOpenItems] = useState>(new Set(['queues', 'topics']))
+
+ const toggleBranch = (key: string) => {
+ setOpenItems((prev) => {
+ const next = new Set(prev)
+ if (next.has(key)) next.delete(key)
+ else next.add(key)
+ return next
+ })
+ }
+
+ const label = (text: string) => (compact ? text.slice(0, 1).toUpperCase() : text)
+
+ const openAndNavigateQueues = () => {
+ setOpenItems((prev) => new Set(prev).add('queues'))
+ navigate('/queues')
+ }
+
+ const openAndNavigateTopics = () => {
+ setOpenItems((prev) => new Set(prev).add('topics'))
+ navigate('/topics')
+ }
+
+ const toggleQueues = () => {
+ setOpenItems((prev) => {
+ const next = new Set(prev)
+ if (next.has('queues')) next.delete('queues')
+ else next.add('queues')
+ return next
+ })
+ }
+
+ const handleQueuesToggleClick = (event: MouseEvent) => {
+ event.preventDefault()
+ event.stopPropagation()
+ toggleQueues()
+ }
+
+ const handleTopicsToggleClick = (event: MouseEvent) => {
+ event.preventDefault()
+ event.stopPropagation()
+ toggleBranch('topics')
+ }
+
+
+ return (
+
+ }
+ rightSection={
+
+ {openItems.has('queues') ? : }
+
+ }
+ opened={openItems.has('queues')}
+ onClick={openAndNavigateQueues}
+ >
+ {queuesLoading && }
+ {queueList.map((queue: any) => (
+ {
+ e.stopPropagation()
+ navigate(`/queues/${queue.name}`)
+ }}
+ rightSection={queue.deadLetterMessageCount > 0 ? (
+
+ {queue.deadLetterMessageCount}
+
+ ) : undefined}
+ childrenOffset={compact ? 12 : 16}
+ />
+ ))}
+
+
+ }
+ rightSection={
+
+ {openItems.has('topics') ? : }
+
+ }
+ opened={openItems.has('topics')}
+ onClick={openAndNavigateTopics}
+ >
+ {topicsLoading && }
+ {topicList.map((topic: any) => (
+ toggleBranch(`topic-${topic.name}`)}
+ />
+ ))}
+
+
+ )
+}
+
+
+export default NavTree
diff --git a/app/sb-explorer-ui/src/components/SideDrawer.tsx b/app/sb-explorer-ui/src/components/SideDrawer.tsx
new file mode 100644
index 0000000..9b61701
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/SideDrawer.tsx
@@ -0,0 +1,48 @@
+import { Drawer, Stack, Group, ActionIcon, Title } from '@mantine/core'
+import { IconX } from '@tabler/icons-react'
+import type { ReactNode } from 'react'
+
+interface SideDrawerProps {
+ open: boolean
+ title: ReactNode
+ width?: number
+ onOpenChange: (open: boolean) => void
+ children: ReactNode
+}
+
+const SideDrawer = ({ open, title, width = 540, onOpenChange, children }: SideDrawerProps) => {
+ return (
+ onOpenChange(false)}
+ position="right"
+ size={width}
+ padding="lg"
+ withOverlay
+ overlayProps={{ opacity: 0.4, blur: 2 }}
+ styles={{
+ content: {
+ borderTopLeftRadius: 16,
+ borderBottomLeftRadius: 16,
+ },
+ }}
+ classNames={{
+ content: 'drawer-content',
+ }}
+ >
+
+
+
+ {title}
+
+ onOpenChange(false)}>
+
+
+
+ {children}
+
+
+ )
+}
+
+export default SideDrawer
diff --git a/app/sb-explorer-ui/src/components/StatusPill.tsx b/app/sb-explorer-ui/src/components/StatusPill.tsx
new file mode 100644
index 0000000..a0d43f6
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/StatusPill.tsx
@@ -0,0 +1,19 @@
+import { Badge } from '@mantine/core'
+import type { EntityStatus } from '../api/types'
+
+const statusColor: Record = {
+ Active: 'green',
+ Disabled: 'red',
+ SendDisabled: 'yellow',
+ ReceiveDisabled: 'yellow',
+}
+
+const StatusPill = ({ status }: { status: EntityStatus }) => {
+ return (
+
+ {status}
+
+ )
+}
+
+export default StatusPill
diff --git a/app/sb-explorer-ui/src/components/dialogs/ConfirmActionDialog.tsx b/app/sb-explorer-ui/src/components/dialogs/ConfirmActionDialog.tsx
new file mode 100644
index 0000000..08d90be
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/dialogs/ConfirmActionDialog.tsx
@@ -0,0 +1,27 @@
+import { Modal, Stack, Text, Title } from '@mantine/core'
+import type { ReactNode } from 'react'
+
+interface ConfirmActionDialogProps {
+ open: boolean
+ title: string
+ description?: string
+ children: ReactNode
+ onOpenChange: (open: boolean) => void
+}
+
+const ConfirmActionDialog = ({ open, title, description, children, onOpenChange }: ConfirmActionDialogProps) => {
+ return (
+ onOpenChange(false)} title={{title} } centered>
+
+ {description && (
+
+ {description}
+
+ )}
+ {children}
+
+
+ )
+}
+
+export default ConfirmActionDialog
diff --git a/app/sb-explorer-ui/src/components/dialogs/CreateQueueDialog.tsx b/app/sb-explorer-ui/src/components/dialogs/CreateQueueDialog.tsx
new file mode 100644
index 0000000..bbc2f07
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/dialogs/CreateQueueDialog.tsx
@@ -0,0 +1,98 @@
+import { useEffect, useState } from 'react'
+import { Button, Group, Modal, NumberInput, Stack, TextInput, Title } from '@mantine/core'
+
+interface CreateQueueDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onSubmit: (payload: { name: string; maxDeliveryCount?: number; lockDuration?: string; defaultTtl?: string }) => void
+}
+
+const maxDefaultTtlSeconds = 60 * 60
+
+const parseTimeSpan = (value: string) => {
+ if (!value) return null
+ const [dayPart, restPart] = value.includes('.') ? value.split('.', 2) : [null, value]
+ const [hours, minutes, seconds] = restPart.split(':')
+ if ([hours, minutes, seconds].some((part) => part === undefined)) return null
+ const days = dayPart ? Number(dayPart) : 0
+ const hrs = Number(hours)
+ const mins = Number(minutes)
+ const secs = Number(seconds)
+ if ([days, hrs, mins, secs].some((part) => Number.isNaN(part) || part < 0)) return null
+ if (mins > 59 || secs > 59) return null
+ return days * 86400 + hrs * 3600 + mins * 60 + secs
+}
+
+const CreateQueueDialog = ({ open, onOpenChange, onSubmit }: CreateQueueDialogProps) => {
+ const [name, setName] = useState('')
+ const [maxDeliveryCount, setMaxDeliveryCount] = useState('10')
+ const [lockDuration, setLockDuration] = useState('00:01:00')
+ const [defaultTtl, setDefaultTtl] = useState('01:00:00')
+ const [defaultTtlError, setDefaultTtlError] = useState(null)
+
+ useEffect(() => {
+ if (!open) {
+ setName('')
+ setMaxDeliveryCount('10')
+ setLockDuration('00:01:00')
+ setDefaultTtl('01:00:00')
+ setDefaultTtlError(null)
+ }
+ }, [open])
+
+ const handleSubmit = () => {
+ if (!name) return
+ const ttlSeconds = parseTimeSpan(defaultTtl)
+ if (ttlSeconds === null) {
+ setDefaultTtlError('Use hh:mm:ss format (max 01:00:00).')
+ return
+ }
+ if (ttlSeconds > maxDefaultTtlSeconds) {
+ setDefaultTtlError('Maximum is 01:00:00.')
+ return
+ }
+ setDefaultTtlError(null)
+ onSubmit({
+ name,
+ maxDeliveryCount: maxDeliveryCount ? Number(maxDeliveryCount) : undefined,
+ lockDuration,
+ defaultTtl,
+ })
+ }
+
+ return (
+ onOpenChange(false)} title={Create queue } centered>
+
+ setName(e.currentTarget.value)} data-autofocus />
+ setMaxDeliveryCount(String(value ?? ''))}
+ min={1}
+ />
+ setLockDuration(e.currentTarget.value)} />
+ {
+ setDefaultTtl(e.currentTarget.value)
+ if (defaultTtlError) setDefaultTtlError(null)
+ }}
+ error={defaultTtlError}
+ />
+
+ onOpenChange(false)}>
+ Cancel
+
+
+ Create
+
+
+
+
+ )
+}
+
+export default CreateQueueDialog
diff --git a/app/sb-explorer-ui/src/components/dialogs/CreateSubscriptionDialog.tsx b/app/sb-explorer-ui/src/components/dialogs/CreateSubscriptionDialog.tsx
new file mode 100644
index 0000000..f8a79bd
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/dialogs/CreateSubscriptionDialog.tsx
@@ -0,0 +1,60 @@
+import { useEffect, useState } from 'react'
+import { Button, Group, Modal, NumberInput, Stack, TextInput, Title } from '@mantine/core'
+
+interface CreateSubscriptionDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onSubmit: (payload: { name: string; maxDeliveryCount?: number; lockDuration?: string; defaultTtl?: string }) => void
+}
+
+const CreateSubscriptionDialog = ({ open, onOpenChange, onSubmit }: CreateSubscriptionDialogProps) => {
+ const [name, setName] = useState('')
+ const [maxDeliveryCount, setMaxDeliveryCount] = useState('10')
+ const [lockDuration, setLockDuration] = useState('00:01:00')
+ const [defaultTtl, setDefaultTtl] = useState('1.00:00:00')
+
+ useEffect(() => {
+ if (!open) {
+ setName('')
+ setMaxDeliveryCount('10')
+ setLockDuration('00:01:00')
+ setDefaultTtl('1.00:00:00')
+ }
+ }, [open])
+
+ const handleSubmit = () => {
+ if (!name) return
+ onSubmit({
+ name,
+ maxDeliveryCount: maxDeliveryCount ? Number(maxDeliveryCount) : undefined,
+ lockDuration,
+ defaultTtl,
+ })
+ }
+
+ return (
+ onOpenChange(false)} title={Create subscription } centered>
+
+ setName(e.currentTarget.value)} data-autofocus />
+ setMaxDeliveryCount(String(value ?? ''))}
+ min={1}
+ />
+ setLockDuration(e.currentTarget.value)} />
+ setDefaultTtl(e.currentTarget.value)} />
+
+ onOpenChange(false)}>
+ Cancel
+
+
+ Create
+
+
+
+
+ )
+}
+
+export default CreateSubscriptionDialog
diff --git a/app/sb-explorer-ui/src/components/dialogs/CreateTopicDialog.tsx b/app/sb-explorer-ui/src/components/dialogs/CreateTopicDialog.tsx
new file mode 100644
index 0000000..d606506
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/dialogs/CreateTopicDialog.tsx
@@ -0,0 +1,39 @@
+import { useEffect, useState } from 'react'
+import { Button, Group, Modal, Stack, TextInput, Title } from '@mantine/core'
+
+interface CreateTopicDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onSubmit: (payload: { name: string }) => void
+}
+
+const CreateTopicDialog = ({ open, onOpenChange, onSubmit }: CreateTopicDialogProps) => {
+ const [name, setName] = useState('')
+
+ useEffect(() => {
+ if (!open) setName('')
+ }, [open])
+
+ const handleSubmit = () => {
+ if (!name) return
+ onSubmit({ name })
+ }
+
+ return (
+ onOpenChange(false)} title={Create topic } centered>
+
+ setName(e.currentTarget.value)} data-autofocus />
+
+ onOpenChange(false)}>
+ Cancel
+
+
+ Create
+
+
+
+
+ )
+}
+
+export default CreateTopicDialog
diff --git a/app/sb-explorer-ui/src/components/dialogs/SendMessageDialog.tsx b/app/sb-explorer-ui/src/components/dialogs/SendMessageDialog.tsx
new file mode 100644
index 0000000..793396f
--- /dev/null
+++ b/app/sb-explorer-ui/src/components/dialogs/SendMessageDialog.tsx
@@ -0,0 +1,116 @@
+import { useEffect, useMemo, useState } from 'react'
+import { Button, Group, Modal, Stack, TextInput, Title } from '@mantine/core'
+import Editor from '@monaco-editor/react'
+import MessagePropertiesTable from '../MessagePropertiesTable'
+
+interface SendMessageDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ onSubmit: (payload: { body: string; contentType?: string; userProperties?: Record }) => void
+ theme: 'light' | 'dark'
+}
+
+const SendMessageDialog = ({ open, onOpenChange, onSubmit, theme }: SendMessageDialogProps) => {
+ const [body, setBody] = useState('{}')
+ const [contentType, setContentType] = useState('application/json')
+ const [userProps, setUserProps] = useState>({})
+ const [kvKey, setKvKey] = useState('')
+ const [kvValue, setKvValue] = useState('')
+
+ useEffect(() => {
+ if (!open) {
+ setBody('{}')
+ setUserProps({})
+ setKvKey('')
+ setKvValue('')
+ }
+ }, [open])
+
+ const parsedUserProps = useMemo(() => {
+ const entries = Object.entries(userProps)
+ const result: Record = {}
+ entries.forEach(([k, v]) => {
+ if (k) result[k] = v
+ })
+ return result
+ }, [userProps])
+
+ const handleAddKv = () => {
+ if (!kvKey) return
+ setUserProps((prev) => ({ ...prev, [kvKey]: kvValue }))
+ setKvKey('')
+ setKvValue('')
+ }
+
+ const handleDeleteKv = (key: string) => {
+ setUserProps((prev) => {
+ const next = { ...prev }
+ delete next[key]
+ return next
+ })
+ }
+
+ const handleSubmit = () => {
+ if (contentType.includes('json')) {
+ try {
+ JSON.parse(body || '{}')
+ } catch (_err) {
+ alert('Body must be valid JSON')
+ return
+ }
+ }
+ onSubmit({ body, contentType, userProperties: parsedUserProps })
+ }
+
+ return (
+ onOpenChange(false)} title={Send message } size="lg" centered>
+
+ setContentType(e.currentTarget.value)} />
+
+ setBody(e.currentTarget.value)} />
+
+ setBody(value ?? '')}
+ theme={theme === 'dark' ? 'vs-dark' : 'vs'}
+ options={{ minimap: { enabled: false }, lineNumbers: 'off', wordWrap: 'on' }}
+ />
+
+
+
+
+
+ setKvKey(e.currentTarget.value)} flex={1} />
+ setKvValue(e.currentTarget.value)} flex={1} />
+
+ Add
+
+
+
+ {Object.keys(parsedUserProps).length > 0 && (
+
+ {Object.keys(parsedUserProps).map((k) => (
+ handleDeleteKv(k)}>
+ Remove {k}
+
+ ))}
+
+ )}
+
+
+
+ onOpenChange(false)}>
+ Cancel
+
+
+ Send
+
+
+
+
+ )
+}
+
+export default SendMessageDialog
diff --git a/app/sb-explorer-ui/src/index.css b/app/sb-explorer-ui/src/index.css
new file mode 100644
index 0000000..aa83cf0
--- /dev/null
+++ b/app/sb-explorer-ui/src/index.css
@@ -0,0 +1,75 @@
+:root {
+ font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ background-color: var(--mantine-color-body);
+ color: var(--mantine-color-text);
+}
+
+#root {
+ min-height: 100vh;
+}
+
+:root[data-mantine-color-scheme='light'] {
+ color-scheme: light;
+}
+
+:root[data-mantine-color-scheme='dark'] {
+ color-scheme: dark;
+}
+
+html, body {
+ width: 100%;
+ overflow-x: hidden;
+}
+
+.drawer-content {
+ background-color: var(--mantine-color-body);
+ color: inherit;
+}
+
+.nav-tree-link {
+ --nl-bg: transparent;
+ --nl-hover: transparent;
+ --nl-color: inherit;
+ color: inherit;
+ background-color: transparent;
+ transition: background-color 160ms ease, box-shadow 160ms ease;
+}
+
+:root[data-mantine-color-scheme='light'] .nav-tree-link:hover,
+:root[data-mantine-color-scheme='light'] .nav-tree-link[data-hovered] {
+ background-color: rgba(255, 255, 255, 0.32);
+ box-shadow: inset 3px 0 0 rgba(255, 255, 255, 0.6);
+}
+
+:root[data-mantine-color-scheme='dark'] .nav-tree-link:hover,
+:root[data-mantine-color-scheme='dark'] .nav-tree-link[data-hovered] {
+ background-color: rgba(255, 255, 255, 0.2);
+ box-shadow: inset 3px 0 0 rgba(255, 255, 255, 0.4);
+}
+
+:root[data-mantine-color-scheme='light'] .nav-tree-link[data-active],
+:root[data-mantine-color-scheme='light'] .nav-tree-link[aria-current='page'] {
+ background-color: rgba(255, 255, 255, 0.42);
+ box-shadow: inset 3px 0 0 rgba(255, 255, 255, 0.8);
+}
+
+:root[data-mantine-color-scheme='dark'] .nav-tree-link[data-active],
+:root[data-mantine-color-scheme='dark'] .nav-tree-link[aria-current='page'] {
+ background-color: rgba(255, 255, 255, 0.32);
+ box-shadow: inset 3px 0 0 rgba(255, 255, 255, 0.6);
+}
diff --git a/app/sb-explorer-ui/src/main.tsx b/app/sb-explorer-ui/src/main.tsx
new file mode 100644
index 0000000..71fc35a
--- /dev/null
+++ b/app/sb-explorer-ui/src/main.tsx
@@ -0,0 +1,26 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
+import '@mantine/core/styles.css'
+import '@mantine/notifications/styles.css'
+import './index.css'
+import App from './App'
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 5000,
+ refetchOnWindowFocus: false,
+ },
+ },
+})
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+ ,
+)
diff --git a/app/sb-explorer-ui/src/routes/AppShell.tsx b/app/sb-explorer-ui/src/routes/AppShell.tsx
new file mode 100644
index 0000000..dfc9fe7
--- /dev/null
+++ b/app/sb-explorer-ui/src/routes/AppShell.tsx
@@ -0,0 +1,227 @@
+import { useMemo, useState, type ReactNode } from 'react'
+import { Outlet } from 'react-router-dom'
+import {
+ ActionIcon,
+ AppShell as MantineAppShell,
+ Badge,
+ Box,
+ Container,
+ Divider,
+ Group,
+ Stack,
+ Text,
+ Tooltip,
+} from '@mantine/core'
+import {
+ IconLayoutSidebarLeftCollapse,
+ IconLayoutSidebarLeftExpand,
+ IconMoon,
+ IconRefresh,
+ IconSun,
+} from '@tabler/icons-react'
+
+import NavTree from '../components/NavTree'
+import MockBanner from '../components/MockBanner'
+import { useAppContext } from '../App'
+
+const ShellHeader = ({ actions, chrome }: { actions?: ReactNode; chrome: { headerBg: string; headerText: string } }) => {
+ const { theme, toggleTheme } = useAppContext()
+ const isLight = theme === 'light'
+
+ return (
+
+
+
+ SB
+
+
+
+ Service Bus Explorer
+
+
+ Emulator
+
+
+
+
+
+ {actions}
+
+ window.location.reload()}
+ style={{ color: chrome.headerText }}
+ >
+
+
+
+
+
+ {isLight ? : }
+
+
+
+
+ )
+}
+
+function AppShell() {
+ const { theme } = useAppContext()
+ const [collapsed, setCollapsed] = useState(false)
+
+ const chrome = useMemo(
+ () =>
+ theme === 'light'
+ ? {
+ headerBg: 'linear-gradient(120deg, #0f65c9 0%, #0b78e3 60%, #0a8cff 100%)',
+ headerText: '#f8fbff',
+ subBg: '#f3f6fb',
+ subText: '#2c3547',
+ divider: '#d6dce7',
+ sidebarBg: '#0c1626',
+ sidebarText: '#f5f7fb',
+ mainBg: '#eef2f8',
+ mainText: '#0f172a',
+ }
+ : {
+ headerBg: 'linear-gradient(120deg, #0a2d4f 0%, #0b3a66 50%, #0f4c85 100%)',
+ headerText: '#e7f1ff',
+ subBg: '#0f1a28',
+ subText: '#dbe6f7',
+ divider: '#1f2836',
+ sidebarBg: '#0b101b',
+ sidebarText: '#e3e8f5',
+ mainBg: '#0b1220',
+ mainText: '#e4e9f5',
+ },
+ [theme],
+ )
+
+ return (
+
+
+
+
+ Azure Service Bus Emulator
+ setCollapsed((c) => !c)}
+ color="gray.0"
+ hiddenFrom="md"
+ >
+ {collapsed ? : }
+
+
+ }
+ />
+
+
+
+
+ {!collapsed && (
+
+ Navigation
+
+ )}
+
+ setCollapsed((c) => !c)}
+ style={{ color: chrome.sidebarText }}
+ >
+ {collapsed ? : }
+
+
+
+
+
+
+
+
+
+ Home
+
+
+ Service Bus (Emulator)
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default AppShell
diff --git a/app/sb-explorer-ui/src/routes/QueueDetail.tsx b/app/sb-explorer-ui/src/routes/QueueDetail.tsx
new file mode 100644
index 0000000..dbbfa57
--- /dev/null
+++ b/app/sb-explorer-ui/src/routes/QueueDetail.tsx
@@ -0,0 +1,192 @@
+import { useMemo, useState } from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
+import { Button, Group, Loader, Stack, Text } from '@mantine/core'
+import { notifications } from '@mantine/notifications'
+import EntityHeader from '../components/EntityHeader'
+import EntityOverviewCard from '../components/EntityOverviewCard'
+import MessageDetailPanel from '../components/MessageDetailPanel'
+import MessageGrid from '../components/MessageGrid'
+import MessageTabs from '../components/MessageTabs'
+import ConfirmActionDialog from '../components/dialogs/ConfirmActionDialog'
+import SendMessageDialog from '../components/dialogs/SendMessageDialog'
+import {
+ useBulkDlqDelete,
+ useDeleteQueue,
+ useMessages,
+ useQueues,
+ useSendMessage,
+} from '../api/hooks'
+import type { MessageState, QueueInfo } from '../api/types'
+import { useAppContext } from '../App'
+
+const QueueDetail = () => {
+ const { name } = useParams()
+ const navigate = useNavigate()
+ const { theme } = useAppContext()
+ const { data: queues, isLoading } = useQueues()
+ const queueList = (Array.isArray(queues) ? queues : queues ? Object.values(queues as any) : []) as QueueInfo[]
+ const queue = useMemo(() => queueList.find((q) => q.name === name), [queueList, name])
+
+ const [messageState, setMessageState] = useState('active')
+ const [skip, setSkip] = useState(0)
+ const take = 25
+ const [selectedIds, setSelectedIds] = useState([])
+ const [inspect, setInspect] = useState()
+ const [sendOpen, setSendOpen] = useState(false)
+ const [deleteOpen, setDeleteOpen] = useState(false)
+
+ const messages = useMessages({
+ scope: { type: 'queue', name: name ?? '' },
+ state: messageState,
+ skip,
+ take,
+ enabled: !!name,
+ })
+
+ const bulkDelete = useBulkDlqDelete()
+ const sendMessage = useSendMessage()
+ const deleteQueue = useDeleteQueue()
+
+ const inspectingMessage = messages.data?.items?.find((m: any) => m.messageId === inspect)
+
+ const handleDelete = async () => {
+ if (!name) return
+ await deleteQueue.mutateAsync(name)
+ setDeleteOpen(false)
+ navigate('/queues')
+ }
+
+ if (!name) return No queue selected.
+ if (isLoading && !queue) return
+ if (!queue) return Queue not found.
+
+ return (
+
+ setSendOpen(true)}
+ onDelete={() => setDeleteOpen(true)}
+ />
+
+
+
+
+ {
+ setMessageState(state)
+ setSelectedIds([])
+ setSkip(0)
+ }}
+ activeCount={queue.activeMessageCount}
+ deadLetterCount={queue.deadLetterMessageCount}
+ />
+
+
+ Messages
+ {messageState === 'deadletter' && (
+
+ bulkDelete.mutate(
+ {
+ scope: { type: 'queue', name },
+ messageIds: selectedIds,
+ },
+ {
+ onSuccess: () => {
+ notifications.show({
+ title: 'DLQ cleared',
+ message: `Deleted ${selectedIds.length} message${selectedIds.length === 1 ? '' : 's'}.`,
+ color: 'green',
+ })
+ setSelectedIds([])
+ },
+ onError: (error) => {
+ notifications.show({
+ title: 'DLQ delete failed',
+ message: error instanceof Error ? error.message : 'Unable to delete DLQ messages.',
+ color: 'red',
+ })
+ },
+ },
+ )
+ }
+ >
+ Delete selected DLQ
+
+ )}
+
+
+ {
+ setSkip(Math.max(0, next))
+ setSelectedIds([])
+ }}
+ onInspect={(msg) => setInspect(msg.messageId)}
+ />
+
+
+ !open && setInspect(undefined)} />
+
+
+ sendMessage.mutate(
+ {
+ scope: { type: 'queue', name },
+ ...payload,
+ },
+ {
+ onSuccess: () => {
+ setSendOpen(false)
+ notifications.show({ title: 'Message sent', message: `Queued to ${name}`, color: 'green' })
+ },
+ },
+ )
+ }
+ />
+
+
+
+ setDeleteOpen(false)}>
+ Cancel
+
+
+ Delete
+
+
+
+
+ )
+}
+
+export default QueueDetail
diff --git a/app/sb-explorer-ui/src/routes/Queues.tsx b/app/sb-explorer-ui/src/routes/Queues.tsx
new file mode 100644
index 0000000..02f7a09
--- /dev/null
+++ b/app/sb-explorer-ui/src/routes/Queues.tsx
@@ -0,0 +1,81 @@
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { Button, Group, Stack, Text } from '@mantine/core'
+import { notifications } from '@mantine/notifications'
+import axios from 'axios'
+import EntityTable from '../components/EntityTable'
+import ConfirmActionDialog from '../components/dialogs/ConfirmActionDialog'
+import CreateQueueDialog from '../components/dialogs/CreateQueueDialog'
+import { useCreateQueue, useDeleteQueue, useQueues } from '../api/hooks'
+import type { QueueInfo } from '../api/types'
+
+const Queues = () => {
+ const navigate = useNavigate()
+ const { data: queues, isLoading, refetch } = useQueues()
+ const createQueue = useCreateQueue()
+ const deleteQueue = useDeleteQueue()
+ const [createOpen, setCreateOpen] = useState(false)
+ const [deleteTarget, setDeleteTarget] = useState(null)
+
+ const handleCreate = async (payload: { name: string; maxDeliveryCount?: number; lockDuration?: string; defaultTtl?: string }) => {
+ try {
+ await createQueue.mutateAsync(payload)
+ setCreateOpen(false)
+ refetch()
+ } catch (error) {
+ const responseData = axios.isAxiosError(error)
+ ? (error.response?.data as { detail?: string; error?: string } | undefined)
+ : undefined
+ const message = responseData?.detail || responseData?.error || (error instanceof Error ? error.message : 'Unable to create queue.')
+ notifications.show({ title: 'Create queue failed', message, color: 'red' })
+ }
+ }
+
+ const handleDelete = async () => {
+ if (!deleteTarget) return
+ await deleteQueue.mutateAsync(deleteTarget.name)
+ setDeleteTarget(null)
+ refetch()
+ }
+
+ return (
+
+ setCreateOpen(true)}
+ onRowClick={(q) => navigate(`/queues/${q.name}`)}
+ emptyState={
+
+ No queues yet.
+ setCreateOpen(true)}>
+ Create one
+
+
+ }
+ />
+
+
+
+ !open && setDeleteTarget(null)}
+ title={`Delete queue ${deleteTarget?.name ?? ''}`}
+ description="This will remove the queue and any messages."
+ >
+
+ setDeleteTarget(null)}>
+ Cancel
+
+
+ Delete
+
+
+
+
+ )
+}
+
+export default Queues
diff --git a/app/sb-explorer-ui/src/routes/TopicDetail.tsx b/app/sb-explorer-ui/src/routes/TopicDetail.tsx
new file mode 100644
index 0000000..6f2ad6c
--- /dev/null
+++ b/app/sb-explorer-ui/src/routes/TopicDetail.tsx
@@ -0,0 +1,292 @@
+import { useMemo, useState } from 'react'
+import { useNavigate, useParams } from 'react-router-dom'
+import { Button, Group, Loader, Paper, Stack, Table, Text } from '@mantine/core'
+import { notifications } from '@mantine/notifications'
+import EntityHeader from '../components/EntityHeader'
+import EntityOverviewCard from '../components/EntityOverviewCard'
+import MessageDetailPanel from '../components/MessageDetailPanel'
+import MessageGrid from '../components/MessageGrid'
+import MessageTabs from '../components/MessageTabs'
+import ConfirmActionDialog from '../components/dialogs/ConfirmActionDialog'
+import CreateSubscriptionDialog from '../components/dialogs/CreateSubscriptionDialog'
+import SendMessageDialog from '../components/dialogs/SendMessageDialog'
+import {
+ useBulkDlqDelete,
+ useCreateSubscription,
+ useDeleteSubscription,
+ useDeleteTopic,
+ useMessages,
+ useSendMessage,
+ useSubscriptions,
+ useTopics,
+} from '../api/hooks'
+import type { MessageState, TopicInfo } from '../api/types'
+import StatusPill from '../components/StatusPill'
+import { useAppContext } from '../App'
+
+const TopicDetail = () => {
+ const { name, subscription } = useParams()
+ const navigate = useNavigate()
+ const { theme } = useAppContext()
+ const { data: topics, isLoading } = useTopics()
+ const topicList = (Array.isArray(topics) ? topics : topics ? Object.values(topics as any) : []) as TopicInfo[]
+ const topic = useMemo(() => topicList.find((t) => t.name === name), [topicList, name])
+
+ const { data: subs, refetch: refetchSubs } = useSubscriptions(name ?? '', true)
+ const subsList = (Array.isArray(subs) ? subs : subs ? Object.values(subs as any) : []) as typeof subs
+ const sub = useMemo(() => subsList?.find((s: any) => s.name === subscription), [subsList, subscription])
+
+ const [messageState, setMessageState] = useState('active')
+ const [skip, setSkip] = useState(0)
+ const take = 25
+ const [selectedIds, setSelectedIds] = useState([])
+ const [inspect, setInspect] = useState()
+ const [sendOpen, setSendOpen] = useState(false)
+ const [createSubOpen, setCreateSubOpen] = useState(false)
+ const [deleteTopicOpen, setDeleteTopicOpen] = useState(false)
+ const [deleteSubOpen, setDeleteSubOpen] = useState(false)
+
+ const messageScope = useMemo(
+ () => ({ type: 'subscription', topic: name ?? '', subscription: subscription ?? '' } as const),
+ [name, subscription],
+ )
+
+ const messages = useMessages({
+ scope: messageScope,
+ state: messageState,
+ skip,
+ take,
+ enabled: !!name && !!subscription,
+ })
+
+ const bulkDelete = useBulkDlqDelete()
+ const sendMessage = useSendMessage()
+ const createSubscription = useCreateSubscription(name ?? '')
+ const deleteSubscription = useDeleteSubscription(name ?? '')
+ const deleteTopic = useDeleteTopic()
+
+ const inspectingMessage = messages?.data?.items?.find((m) => m.messageId === inspect)
+
+ const handleCreateSub = async (payload: { name: string; maxDeliveryCount?: number; lockDuration?: string; defaultTtl?: string }) => {
+ await createSubscription.mutateAsync(payload)
+ setCreateSubOpen(false)
+ refetchSubs()
+ }
+
+ const handleDeleteSub = async () => {
+ if (!subscription) return
+ await deleteSubscription.mutateAsync(subscription)
+ setDeleteSubOpen(false)
+ navigate(`/topics/${name}`)
+ refetchSubs()
+ }
+
+ const handleDeleteTopic = async () => {
+ if (!name) return
+ await deleteTopic.mutateAsync(name)
+ setDeleteTopicOpen(false)
+ navigate('/topics')
+ }
+
+ if (!name) return No topic selected.
+ if (isLoading && !topic) return
+ if (!topic) return Topic not found.
+
+ const renderSubscriptionList = () => (
+
+
+ Subscriptions
+ setCreateSubOpen(true)}>Create subscription
+
+
+
+
+ Name
+ Status
+ Active
+ DLQ
+
+
+
+
+ {subs?.map((s) => (
+ navigate(`/topics/${name}/${s.name}`)}>
+ {s.name}
+
+
+
+ {s.activeMessageCount ?? 0}
+ {s.deadLetterMessageCount ?? 0}
+
+ { e.stopPropagation(); navigate(`/topics/${name}/${s.name}`) }}>
+ Open
+
+
+
+ ))}
+
+
+
+ )
+
+ const showSubscriptionDetail = !!subscription && sub
+
+ return (
+
+ setSendOpen(true)}
+ onDelete={() => (showSubscriptionDetail ? setDeleteSubOpen(true) : setDeleteTopicOpen(true))}
+ onCreateSubscription={showSubscriptionDetail ? undefined : () => setCreateSubOpen(true)}
+ />
+
+
+
+ {showSubscriptionDetail ? (
+
+ {
+ setMessageState(state)
+ setSelectedIds([])
+ setSkip(0)
+ }}
+ activeCount={sub?.activeMessageCount}
+ deadLetterCount={sub?.deadLetterMessageCount}
+ />
+
+
+ Messages
+ {messageState === 'deadletter' && (
+
+ bulkDelete.mutate(
+ {
+ scope: { type: 'subscription', topic: name, subscription },
+ messageIds: selectedIds,
+ },
+ {
+ onSuccess: () => {
+ notifications.show({
+ title: 'DLQ cleared',
+ message: `Deleted ${selectedIds.length} message${selectedIds.length === 1 ? '' : 's'}.`,
+ color: 'green',
+ })
+ setSelectedIds([])
+ },
+ onError: (error) => {
+ notifications.show({
+ title: 'DLQ delete failed',
+ message: error instanceof Error ? error.message : 'Unable to delete DLQ messages.',
+ color: 'red',
+ })
+ },
+ },
+ )
+ }
+ >
+ Delete selected DLQ
+
+ )}
+
+
+ {
+ setSkip(Math.max(0, next))
+ setSelectedIds([])
+ }}
+ onInspect={(msg) => setInspect(msg.messageId)}
+ />
+
+ ) : (
+ renderSubscriptionList()
+ )}
+
+ !open && setInspect(undefined)} />
+
+
+ sendMessage.mutate(
+ {
+ scope: showSubscriptionDetail ? { type: 'topic', name: topic.name } : { type: 'topic', name: topic.name },
+ ...payload,
+ },
+ {
+ onSuccess: () => {
+ setSendOpen(false)
+ notifications.show({ title: 'Message sent', message: `Sent to ${showSubscriptionDetail ? 'subscription' : 'topic'}`, color: 'green' })
+ },
+ },
+ )
+ }
+ />
+
+
+
+
+
+ setDeleteTopicOpen(false)}>
+ Cancel
+
+
+ Delete
+
+
+
+
+
+
+ setDeleteSubOpen(false)}>
+ Cancel
+
+
+ Delete
+
+
+
+
+ )
+}
+
+export default TopicDetail
diff --git a/app/sb-explorer-ui/src/routes/Topics.tsx b/app/sb-explorer-ui/src/routes/Topics.tsx
new file mode 100644
index 0000000..7c2348d
--- /dev/null
+++ b/app/sb-explorer-ui/src/routes/Topics.tsx
@@ -0,0 +1,71 @@
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { Button, Group, Stack, Text } from '@mantine/core'
+import EntityTable from '../components/EntityTable'
+import ConfirmActionDialog from '../components/dialogs/ConfirmActionDialog'
+import CreateTopicDialog from '../components/dialogs/CreateTopicDialog'
+import { useCreateTopic, useDeleteTopic, useTopics } from '../api/hooks'
+import type { TopicInfo } from '../api/types'
+
+const Topics = () => {
+ const navigate = useNavigate()
+ const { data: topics, isLoading, refetch } = useTopics()
+ const createTopic = useCreateTopic()
+ const deleteTopic = useDeleteTopic()
+ const [createOpen, setCreateOpen] = useState(false)
+ const [deleteTarget, setDeleteTarget] = useState(null)
+
+ const handleCreate = async (payload: { name: string }) => {
+ await createTopic.mutateAsync(payload)
+ setCreateOpen(false)
+ refetch()
+ }
+
+ const handleDelete = async () => {
+ if (!deleteTarget) return
+ await deleteTopic.mutateAsync(deleteTarget.name)
+ setDeleteTarget(null)
+ refetch()
+ }
+
+ return (
+
+ setCreateOpen(true)}
+ onRowClick={(t) => navigate(`/topics/${t.name}`)}
+ emptyState={
+
+ No topics yet.
+ setCreateOpen(true)}>
+ Create one
+
+
+ }
+ />
+
+
+
+ !open && setDeleteTarget(null)}
+ title={`Delete topic ${deleteTarget?.name ?? ''}`}
+ description="This will remove the topic and its messages."
+ >
+
+ setDeleteTarget(null)}>
+ Cancel
+
+
+ Delete
+
+
+
+
+ )
+}
+
+export default Topics
diff --git a/app/sb-explorer-ui/src/theme.ts b/app/sb-explorer-ui/src/theme.ts
new file mode 100644
index 0000000..8a4eb46
--- /dev/null
+++ b/app/sb-explorer-ui/src/theme.ts
@@ -0,0 +1,99 @@
+import { createTheme } from '@mantine/core'
+import type { BrandVariants, Theme } from '@fluentui/react-components'
+import { createDarkTheme, createLightTheme } from '@fluentui/react-components'
+
+// Mantine themes
+export const mantineLight = createTheme({
+ colors: {
+ brand: [
+ '#e5f2ff',
+ '#c9e0ff',
+ '#9fc7ff',
+ '#73abff',
+ '#4c8fff',
+ '#2f79ff',
+ '#1d6cff',
+ '#0b60ff',
+ '#0047cc',
+ '#003399',
+ ],
+ },
+ primaryColor: 'brand',
+ primaryShade: 5,
+ fontFamily: "'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif",
+ defaultRadius: 'md',
+ shadows: {
+ sm: '0 4px 14px rgba(0,0,0,0.07)',
+ md: '0 8px 24px rgba(0,0,0,0.08)',
+ },
+ headings: { fontFamily: "'Segoe UI', 'Segoe UI Web (West European)', sans-serif" },
+})
+
+export const mantineDark = createTheme({
+ colors: {
+ brand: [
+ '#e5f2ff',
+ '#c9e0ff',
+ '#9fc7ff',
+ '#73abff',
+ '#4c8fff',
+ '#2f79ff',
+ '#1d6cff',
+ '#0b60ff',
+ '#0047cc',
+ '#003399',
+ ],
+ },
+ primaryColor: 'brand',
+ primaryShade: 5,
+ fontFamily: "'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif",
+ defaultRadius: 'md',
+ shadows: {
+ sm: '0 4px 14px rgba(0,0,0,0.35)',
+ md: '0 8px 24px rgba(0,0,0,0.5)',
+ },
+ headings: { fontFamily: "'Segoe UI', 'Segoe UI Web (West European)', sans-serif" },
+})
+
+// Fluent themes (for existing Fluent components)
+const brand: BrandVariants = {
+ 10: '#061724',
+ 20: '#082338',
+ 30: '#0a2e4a',
+ 40: '#0c3b5f',
+ 50: '#0e4775',
+ 60: '#0f548c',
+ 70: '#115ea3',
+ 80: '#0f6ebe',
+ 90: '#0d7fcf',
+ 100: '#0b88d0',
+ 110: '#0a96dd',
+ 120: '#0ba8e0',
+ 130: '#0db5e4',
+ 140: '#1bc3ee',
+ 150: '#35d0f8',
+ 160: '#4fdbff',
+}
+
+const fluentBaseLight = createLightTheme(brand)
+const fluentBaseDark = createDarkTheme(brand)
+
+export const fluentLightTheme: Theme = {
+ ...fluentBaseLight,
+ colorNeutralBackground1: '#eef1f7',
+ colorNeutralBackground2: '#ffffff',
+ colorNeutralStroke1: '#d0d7e2',
+ shadow4: '0 8px 24px rgba(0,0,0,0.08)',
+ shadow8: '0 12px 32px rgba(0,0,0,0.12)',
+ fontFamilyBase: "'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif",
+}
+
+export const fluentDarkTheme: Theme = {
+ ...fluentBaseDark,
+ colorNeutralBackground1: '#0f1624',
+ colorNeutralBackground2: '#111820',
+ colorNeutralStroke1: '#1f2633',
+ shadow4: '0 8px 24px rgba(0,0,0,0.5)',
+ shadow8: '0 12px 32px rgba(0,0,0,0.6)',
+ fontFamilyBase: "'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif",
+}
diff --git a/app/sb-explorer-ui/src/vite-env.d.ts b/app/sb-explorer-ui/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/app/sb-explorer-ui/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/app/sb-explorer-ui/tsconfig.app.json b/app/sb-explorer-ui/tsconfig.app.json
new file mode 100644
index 0000000..a9b5a59
--- /dev/null
+++ b/app/sb-explorer-ui/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/app/sb-explorer-ui/tsconfig.json b/app/sb-explorer-ui/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/app/sb-explorer-ui/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/app/sb-explorer-ui/tsconfig.node.json b/app/sb-explorer-ui/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/app/sb-explorer-ui/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/app/sb-explorer-ui/vite.config.ts b/app/sb-explorer-ui/vite.config.ts
new file mode 100644
index 0000000..2ea0e84
--- /dev/null
+++ b/app/sb-explorer-ui/vite.config.ts
@@ -0,0 +1,40 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ strictPort: true,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:5173',
+ changeOrigin: true,
+ secure: false,
+ },
+ '/scalar': {
+ target: 'http://localhost:5123',
+ changeOrigin: true,
+ secure: false,
+ },
+ '/openapi': {
+ target: 'http://localhost:5123',
+ changeOrigin: true,
+ secure: false,
+ },
+ },
+ },
+ build: {
+ target: 'esnext',
+ minify: 'esbuild',
+ cssMinify: true,
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ vendor: ['react', 'react-dom', 'react-router-dom'],
+ },
+ },
+ },
+ },
+})
diff --git a/compose-services.yaml b/compose-services.yaml
index 3718464..7c5df81 100644
--- a/compose-services.yaml
+++ b/compose-services.yaml
@@ -3,10 +3,9 @@ services:
container_name: "servicebus-emulator"
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
pull_policy: always
- volumes:
- - "${CONFIG_PATH}:/ServiceBus_Emulator/ConfigFiles/Config.json"
ports:
- "5672:5672"
+ - "5300:5300"
environment:
SQL_SERVER: sql
MSSQL_SA_PASSWORD: "${SQL_PASSWORD}" # Password should be same as what is set for SQL
@@ -27,17 +26,5 @@ services:
environment:
ACCEPT_EULA: ${ACCEPT_EULA}
MSSQL_SA_PASSWORD: "${SQL_PASSWORD}"
- redis:
- image: redis:8.0-M02-alpine
- restart: always
- ports:
- - "6379:6379"
- command: redis-server --save 60 1 --loglevel warning --requirepass pass@w0rd1
- volumes:
- - ./data:/data
- networks:
- sb-emulator:
- aliases:
- - "redis"
networks:
sb-emulator:
diff --git a/compose.yaml b/compose.yaml
index e0c5ff6..6a63768 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,35 +1,29 @@
services:
- sbexplorer.mud:
- image: ghcr.io/tombiddulph/service-bus-emulator-explorer:main
- build:
- context: .
- dockerfile: Dockerfile
+ sb-explorer:
+ image: ghcr.io/tombiddulph/sbuitest:latest
+# build:
+# context: .
+# dockerfile: src/ServiceBusEmulatorExplorer/Dockerfile
ports:
- "8080:8080"
- "8081:8081"
environment:
- - "ServiceBus__ConnectionString=Endpoint=sb://servicebus-emulator;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
- - "ServiceBus_RefreshIntervalMs=5000"
- - "Redis__ConnectionString=redis:6379,abortConnect=false,ssl=false,password=pass@w0rd1"
- - "ASPNETCORE_ENVIRONMENT=Production"
- - "ASPNETCORE_URLS=https://+:8081;http://+:8080"
- - "ASPNETCORE_Kestrel__Certificates__Default__Password=password"
- - "ASPNETCORE_Kestrel__Certificates__Default__Path=/https/aspnetapp.pfx"
- volumes:
- - ~/.aspnet/https:/https:ro
- - "${CONFIG_PATH}:/app/service-bus-config.json"
+ - "ServiceBus__ConnectionString=Endpoint=sb://servicebus-emulator;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
+ - "ServiceBus__AdministrationConnectionString=Endpoint=sb://servicebus-emulator:5300;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"
+ - "ServiceBus_RefreshIntervalMs=5000"
+ - "ASPNETCORE_ENVIRONMENT=Production"
networks:
- sb-emulator:
- aliases:
- - "sbexplorer.mud"
+ sb-emulator:
+ aliases:
+ - "sb-explorer"
servicebus-emulator:
container_name: "servicebus-emulator"
image: mcr.microsoft.com/azure-messaging/servicebus-emulator:latest
pull_policy: always
- volumes:
- - "${CONFIG_PATH}:/ServiceBus_Emulator/ConfigFiles/Config.json"
+
ports:
- "5672:5672"
+ - "5300:5300"
environment:
SQL_SERVER: sql
MSSQL_SA_PASSWORD: "${SQL_PASSWORD}" # Password should be same as what is set for SQL
@@ -50,17 +44,6 @@
environment:
ACCEPT_EULA: ${ACCEPT_EULA}
MSSQL_SA_PASSWORD: "${SQL_PASSWORD}"
- redis:
- image: redis:8.0-M02-alpine
- restart: always
- ports:
- - '6379:6379'
- command: redis-server --save 60 1 --loglevel warning --requirepass pass@w0rd1
- volumes:
- - ./data:/data
- networks:
- sb-emulator:
- aliases:
- - "redis"
networks:
sb-emulator:
+
diff --git a/docs/queues-overview.png b/docs/queues-overview.png
new file mode 100644
index 0000000..591a82a
Binary files /dev/null and b/docs/queues-overview.png differ
diff --git a/image.png b/image.png
deleted file mode 100644
index 9e265f2..0000000
Binary files a/image.png and /dev/null differ
diff --git a/sb-explorer.slnx b/sb-explorer.slnx
new file mode 100644
index 0000000..c08b55d
--- /dev/null
+++ b/sb-explorer.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/service-bus-config.json b/service-bus-config.json
deleted file mode 100644
index 7b07677..0000000
--- a/service-bus-config.json
+++ /dev/null
@@ -1,149 +0,0 @@
-{
- "UserConfig": {
- "Namespaces": [
- {
- "Name": "serivcebus-emulator",
- "Queues": [
- {
- "Name": "queue.1",
- "Properties": {
- "DeadLetteringOnMessageExpiration": false,
- "DefaultMessageTimeToLive": "PT1H",
- "DuplicateDetectionHistoryTimeWindow": "PT20S",
- "ForwardDeadLetteredMessagesTo": "",
- "ForwardTo": "",
- "LockDuration": "PT1M",
- "MaxDeliveryCount": 10,
- "RequiresDuplicateDetection": false,
- "RequiresSession": false
- }
- },
- {
- "Name": "queue.2",
- "Properties": {
- "DeadLetteringOnMessageExpiration": false,
- "DefaultMessageTimeToLive": "PT1H",
- "DuplicateDetectionHistoryTimeWindow": "PT20S",
- "ForwardDeadLetteredMessagesTo": "",
- "ForwardTo": "",
- "LockDuration": "PT1M",
- "MaxDeliveryCount": 10,
- "RequiresDuplicateDetection": false,
- "RequiresSession": false
- }
- },
- {
- "Name": "queue.3",
- "Properties": {
- "DeadLetteringOnMessageExpiration": false,
- "DefaultMessageTimeToLive": "PT1H",
- "DuplicateDetectionHistoryTimeWindow": "PT20S",
- "ForwardDeadLetteredMessagesTo": "",
- "ForwardTo": "",
- "LockDuration": "PT1M",
- "MaxDeliveryCount": 10,
- "RequiresDuplicateDetection": false,
- "RequiresSession": false
- }
- },
- {
- "Name": "queue.4",
- "Properties": {
- "DeadLetteringOnMessageExpiration": false,
- "DefaultMessageTimeToLive": "PT1H",
- "DuplicateDetectionHistoryTimeWindow": "PT20S",
- "ForwardDeadLetteredMessagesTo": "",
- "ForwardTo": "",
- "LockDuration": "PT1M",
- "MaxDeliveryCount": 10,
- "RequiresDuplicateDetection": false,
- "RequiresSession": false
- }
- }
- ],
- "Topics": [
- {
- "Name": "topic.1",
- "Properties": {
- "DefaultMessageTimeToLive": "PT1H",
- "DuplicateDetectionHistoryTimeWindow": "PT20S",
- "RequiresDuplicateDetection": false
- },
- "Subscriptions": [
- {
- "Name": "subscription.1",
- "Properties": {
- "DeadLetteringOnMessageExpiration": false,
- "DefaultMessageTimeToLive": "PT1H",
- "LockDuration": "PT1M",
- "MaxDeliveryCount": 10,
- "ForwardDeadLetteredMessagesTo": "",
- "ForwardTo": "",
- "RequiresSession": false
- },
- "Rules": [
- {
- "Name": "app-prop-filter-1",
- "Properties": {
- "FilterType": "Correlation",
- "CorrelationFilter": {
- "ContentType": "application/text",
- "CorrelationId": "id1",
- "Label": "subject1",
- "MessageId": "msgid1",
- "ReplyTo": "someQueue",
- "ReplyToSessionId": "sessionId",
- "SessionId": "session1",
- "To": "xyz"
- }
- }
- }
- ]
- },
- {
- "Name": "subscription.2",
- "Properties": {
- "DeadLetteringOnMessageExpiration": false,
- "DefaultMessageTimeToLive": "PT1H",
- "LockDuration": "PT1M",
- "MaxDeliveryCount": 10,
- "ForwardDeadLetteredMessagesTo": "",
- "ForwardTo": "",
- "RequiresSession": false
- },
- "Rules": [
- {
- "Name": "user-prop-filter-1",
- "Properties": {
- "FilterType": "Correlation",
- "CorrelationFilter": {
- "Properties": {
- "prop3": "value3"
- }
- }
- }
- }
- ]
- },
- {
- "Name": "subscription.3",
- "Properties": {
- "DeadLetteringOnMessageExpiration": false,
- "DefaultMessageTimeToLive": "PT1H",
- "LockDuration": "PT1M",
- "MaxDeliveryCount": 10,
- "ForwardDeadLetteredMessagesTo": "",
- "ForwardTo": "",
- "RequiresSession": false
- }
- }
- ]
- }
- ]
- }
- ],
- "Logging": {
- "Type": "Console"
- }
- }
-}
\ No newline at end of file
diff --git a/src/ServiceBusEmulatorExplorer/AppJsonContext.cs b/src/ServiceBusEmulatorExplorer/AppJsonContext.cs
new file mode 100644
index 0000000..7272bf2
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/AppJsonContext.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace ServiceBusEmulatorExplorer;
+
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(QueueInfo))]
+[JsonSerializable(typeof(TopicInfo))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(SubscriptionInfo))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(MessageInfo))]
+[JsonSerializable(typeof(PagedMessages))]
+[JsonSerializable(typeof(SendMessageRequest))]
+[JsonSerializable(typeof(CreateTopicRequest))]
+[JsonSerializable(typeof(CreateSubscriptionRequest))]
+[JsonSerializable(typeof(CreateQueueRequest))]
+[JsonSerializable(typeof(BulkDlqDeleteRequest))]
+public partial class AppJsonContext : JsonSerializerContext;
diff --git a/src/ServiceBusEmulatorExplorer/Dockerfile b/src/ServiceBusEmulatorExplorer/Dockerfile
new file mode 100644
index 0000000..3308c9f
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Dockerfile
@@ -0,0 +1,49 @@
+FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled AS base
+WORKDIR /app
+EXPOSE 8080
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0-noble AS build
+
+# Install Node.js 20 via NodeSource + AOT dependencies
+RUN apt-get update && \
+ apt-get install -y --no-install-recommends ca-certificates curl clang zlib1g-dev && \
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
+ apt-get install -y --no-install-recommends nodejs && \
+ rm -rf /var/lib/apt/lists/*
+
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+
+# Restore .NET dependencies with RID for AOT
+COPY ["src/ServiceBusEmulatorExplorer/ServiceBusEmulatorExplorer.csproj", "src/ServiceBusEmulatorExplorer/"]
+RUN dotnet restore "src/ServiceBusEmulatorExplorer/ServiceBusEmulatorExplorer.csproj" -r linux-x64
+
+# Restore npm dependencies
+COPY ["app/sb-explorer-ui/package.json", "app/sb-explorer-ui/"]
+COPY ["app/sb-explorer-ui/package-lock.json", "app/sb-explorer-ui/"]
+WORKDIR /src/app/sb-explorer-ui
+RUN npm ci --include=dev
+
+# Copy everything else
+WORKDIR /src
+COPY . .
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+ARG VERSION=0.0.0
+ARG ASSEMBLY_VERSION=0.0.0.0
+WORKDIR /src/src/ServiceBusEmulatorExplorer
+RUN dotnet publish "ServiceBusEmulatorExplorer.csproj" -c $BUILD_CONFIGURATION -r linux-x64 \
+ -p:Version=${VERSION} \
+ -p:AssemblyVersion=${ASSEMBLY_VERSION} \
+ -p:FileVersion=${ASSEMBLY_VERSION} \
+ -p:InformationalVersion=${VERSION} \
+ -o /app/publish
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+
+ENV ASPNETCORE_URLS=http://+:8080
+
+ENTRYPOINT ["./ServiceBusEmulatorExplorer.Api"]
\ No newline at end of file
diff --git a/src/ServiceBusEmulatorExplorer/Endpoints/DeadLetterEndpoints.cs b/src/ServiceBusEmulatorExplorer/Endpoints/DeadLetterEndpoints.cs
new file mode 100644
index 0000000..9cb599b
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Endpoints/DeadLetterEndpoints.cs
@@ -0,0 +1,79 @@
+using System.Diagnostics;
+using Azure.Messaging.ServiceBus;
+
+namespace ServiceBusEmulatorExplorer.Endpoints;
+
+public static class DeadLetterEndpoints
+{
+ public static IEndpointRouteBuilder MapDeadLetterEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("deadletter")
+ .WithTags("Dead Letter");
+
+ group.MapPost("/queue/{name}/delete", BulkDeleteQueueDlq)
+ .WithName("BulkDeleteQueueDlq")
+ .WithSummary("Bulk delete queue DLQ")
+ .Produces(StatusCodes.Status200OK);
+
+ group.MapPost("/subscription/{topic}/{sub}/delete", BulkDeleteSubscriptionDlq)
+ .WithName("BulkDeleteSubscriptionDlq")
+ .WithSummary("Bulk delete subscription DLQ")
+ .Produces(StatusCodes.Status200OK);
+
+ return app;
+ }
+
+ private static async Task BulkDeleteQueueDlq(string name, ServiceBusEndpointCache cache,
+ BulkDlqDeleteRequest? request = null)
+ {
+ var receiver = cache.GetReceiver(
+ queue: name,
+ receiverOptions: new()
+ {
+ SubQueue = SubQueue.DeadLetter,
+ ReceiveMode = ServiceBusReceiveMode.ReceiveAndDelete,
+ });
+
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+
+ try
+ {
+ await receiver.ReceiveMessagesAsync(cts.Token).ToListAsync(cts.Token);
+ }
+ catch (Exception e)
+ {
+ Activity.Current?.AddException(e);
+ }
+
+ return Results.Ok();
+ }
+
+ private static async Task BulkDeleteSubscriptionDlq(
+ string topic,
+ string sub,
+ ServiceBusEndpointCache cache,
+ BulkDlqDeleteRequest? request = null)
+ {
+ var receiver = cache.GetTopicReceiver(
+ topic: topic,
+ subscription: sub,
+ receiverOptions: new()
+ {
+ SubQueue = SubQueue.DeadLetter,
+ ReceiveMode = ServiceBusReceiveMode.ReceiveAndDelete,
+ });
+
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+
+ try
+ {
+ await receiver.ReceiveMessagesAsync(cts.Token).ToListAsync(cts.Token);
+ }
+ catch (Exception e)
+ {
+ Activity.Current?.AddException(e);
+ }
+
+ return Results.Ok();
+ }
+}
\ No newline at end of file
diff --git a/src/ServiceBusEmulatorExplorer/Endpoints/QueueEndpoints.cs b/src/ServiceBusEmulatorExplorer/Endpoints/QueueEndpoints.cs
new file mode 100644
index 0000000..f722680
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Endpoints/QueueEndpoints.cs
@@ -0,0 +1,174 @@
+using Azure;
+using Azure.Messaging.ServiceBus;
+using Azure.Messaging.ServiceBus.Administration;
+using Microsoft.AspNetCore.Mvc;
+
+namespace ServiceBusEmulatorExplorer.Endpoints;
+
+public static class QueueEndpoints
+{
+ public static IEndpointRouteBuilder MapQueueEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("queues")
+ .WithTags("Queues");
+
+ group.MapGet("/", ListQueues)
+ .WithName("ListQueues")
+ .WithSummary("List queues")
+ .Produces>();
+
+ group.MapPost("/", CreateQueue)
+ .WithName("CreateQueue")
+ .WithSummary("Create queue")
+ .Produces(StatusCodes.Status200OK);
+
+ group.MapDelete("/{name}", DeleteQueue)
+ .WithName("DeleteQueue")
+ .WithSummary("Delete queue")
+ .Produces(StatusCodes.Status200OK);
+
+ group.MapGet("/{name}/messages", PeekQueueMessages)
+ .WithName("PeekQueueMessages")
+ .WithSummary("Peek queue messages")
+ .Produces();
+
+ group.MapPost("/{name}/messages", SendQueueMessage)
+ .WithName("SendQueueMessage")
+ .WithSummary("Send message to queue")
+ .Produces(StatusCodes.Status200OK);
+
+ return app;
+ }
+
+ private static async Task ListQueues([FromServices] ServiceBusAdministrationClient client)
+ {
+ var queuesRuntimeProperties = client.GetQueuesRuntimePropertiesAsync();
+
+ if (queuesRuntimeProperties is null)
+ {
+ return Results.NotFound();
+ }
+
+ var queues = new List();
+
+ await foreach (var item in queuesRuntimeProperties)
+ {
+ var queue = await client.GetQueueAsync(item.Name)!;
+
+ var queueInfo = new QueueInfo(
+ item.Name,
+ EntityStatus.Active,
+ item.ActiveMessageCount,
+ item.DeadLetterMessageCount,
+ item.ScheduledMessageCount,
+ queue.Value.MaxDeliveryCount,
+ queue.Value.LockDuration.ToString(),
+ queue.Value.DefaultMessageTimeToLive.ToString(),
+ item.CreatedAt);
+
+ queues.Add(queueInfo);
+ }
+
+ return Results.Ok(queues);
+ }
+
+ private static async Task CreateQueue(CreateQueueRequest request, ServiceBusAdministrationClient client)
+ {
+ var options = new CreateQueueOptions(request.Name)
+ {
+ DefaultMessageTimeToLive = request.DefaultTtl switch
+ {
+ _ when TimeSpan.TryParse(request.DefaultTtl, out var parsedTtl) => parsedTtl,
+ _ => TimeSpan.FromDays(14)
+ },
+ LockDuration = request.LockDuration switch
+ {
+ _ when TimeSpan.TryParse(request.LockDuration, out var parsedLockDuration) => parsedLockDuration,
+ _ => TimeSpan.FromMinutes(1)
+ },
+ MaxDeliveryCount = request.MaxDeliveryCount ?? 10
+ };
+
+ Response? response = await client.CreateQueueAsync(options);
+
+ return response switch
+ {
+ { HasValue: true } => Results.Ok(response.Value.Name),
+ _ => Results.Problem("Failed to create queue", statusCode: StatusCodes.Status500InternalServerError, title: "Internal Server Error")
+ };
+ }
+
+ private static async Task DeleteQueue(string name, ServiceBusAdministrationClient client)
+ {
+ var response = await client.DeleteQueueAsync(name);
+
+ return Results.StatusCode(response.Status);
+ }
+
+ private static async Task PeekQueueMessages(
+ string name,
+ [FromQuery] CaseInsensitiveEnum mode,
+ [FromQuery] CaseInsensitiveEnum state,
+ ServiceBusEndpointCache endpointCache,
+ int skip = 0,
+ int take = 25)
+ {
+ var receiverOptions = new ServiceBusReceiverOptions
+ {
+ ReceiveMode = ServiceBusReceiveMode.PeekLock,
+ SubQueue = state == MessageState.Deadletter ? SubQueue.DeadLetter : SubQueue.None
+ };
+
+ var receiver = endpointCache.GetReceiver(name, receiverOptions);
+
+ using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+
+ IReadOnlyList? messages = [];
+ try
+ {
+ messages = await receiver.PeekMessagesAsync(
+ maxMessages: take,
+ fromSequenceNumber: long.MinValue + skip, cancellationToken: cancellationTokenSource.Token);
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+
+ var messageInfos = messages.Select(message => new MessageInfo(
+ message.MessageId,
+ message.Body.ToString().Length <= 50 ? message.Body.ToString() : message.Body.ToString()[..50],
+ message.Body.ToString(),
+ message.EnqueuedTime.UtcDateTime,
+ message.ExpiresAt.UtcDateTime,
+ message.DeliveryCount,
+ message.ContentType,
+ message.SessionId,
+ message.GetRawAmqpMessage().MessageAnnotations.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
+ message.ApplicationProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))).ToList();
+
+ var pagedMessages = new PagedMessages(messageInfos, messageInfos.Count, messageInfos.Count != 0);
+
+ return Results.Ok(pagedMessages);
+ }
+
+ private static async Task SendQueueMessage(string name, SendMessageRequest request,
+ ServiceBusEndpointCache endpointCache)
+ {
+ var message = new ServiceBusMessage(request.Body)
+ {
+ ContentType = request.ContentType,
+ MessageId = Guid.NewGuid().ToString(),
+ };
+
+ foreach (var requestUserProperty in request.UserProperties ?? [])
+ {
+ message.ApplicationProperties[requestUserProperty.Key] = requestUserProperty.Value.ToString();
+ }
+
+ var sender = endpointCache.GetSender(name);
+ await sender.SendMessageAsync(message);
+
+ return Results.Ok();
+ }
+}
diff --git a/src/ServiceBusEmulatorExplorer/Endpoints/SubscriptionEndpoints.cs b/src/ServiceBusEmulatorExplorer/Endpoints/SubscriptionEndpoints.cs
new file mode 100644
index 0000000..8d7df9d
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Endpoints/SubscriptionEndpoints.cs
@@ -0,0 +1,161 @@
+using Azure.Messaging.ServiceBus;
+using Azure.Messaging.ServiceBus.Administration;
+
+namespace ServiceBusEmulatorExplorer.Endpoints;
+
+public static class SubscriptionEndpoints
+{
+ public static IEndpointRouteBuilder MapSubscriptionEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("topics/{topic}/subscriptions")
+ .WithTags("Subscriptions");
+
+ group.MapGet("/", ListSubscriptions)
+ .WithName("ListSubscriptions")
+ .WithSummary("List subscriptions on topic")
+ .Produces>();
+
+ group.MapPost("/", CreateSubscription)
+ .WithName("CreateSubscription")
+ .WithSummary("Create subscription")
+ .Produces(StatusCodes.Status200OK);
+
+ group.MapDelete("/{sub}", DeleteSubscription)
+ .WithName("DeleteSubscription")
+ .WithSummary("Delete subscription")
+ .Produces(StatusCodes.Status200OK);
+
+ group.MapGet("/{sub}/messages", PeekSubscriptionMessages)
+ .WithName("PeekSubscriptionMessages")
+ .WithSummary("Peek subscription messages")
+ .Produces();
+
+ return app;
+ }
+
+ private static async Task ListSubscriptions(string topic, ServiceBusAdministrationClient client)
+ {
+ var subscriptionsRuntimeProperties = client.GetSubscriptionsRuntimePropertiesAsync(topic);
+ if (subscriptionsRuntimeProperties is null)
+ {
+ return Results.NotFound();
+ }
+
+ var subscriptions = new List();
+
+ await foreach (var item in subscriptionsRuntimeProperties)
+ {
+ var subscription = await client.GetSubscriptionRuntimePropertiesAsync(topic, item.SubscriptionName)!;
+
+ if (subscription is null)
+ {
+ continue;
+ }
+
+ var subscriptionInfo = new SubscriptionInfo(
+ item.SubscriptionName,
+ EntityStatus.Active,
+ subscription.Value.ActiveMessageCount,
+ item.DeadLetterMessageCount
+ );
+
+ subscriptions.Add(subscriptionInfo);
+ }
+
+ return Results.Ok(subscriptions);
+ }
+
+ private static async Task CreateSubscription(string topic, CreateSubscriptionRequest request,
+ ServiceBusAdministrationClient client)
+ {
+
+ try
+ {
+ var currentSubscription = await client.GetSubscriptionAsync(topic, request.Name);
+
+ if (currentSubscription is not null)
+ {
+ return Results.Problem(
+ $"Subscription with name '{request.Name}' already exists on topic '{topic}'.",
+ statusCode: StatusCodes.Status409Conflict,
+ title: "Conflict");
+ }
+ }
+ catch (ServiceBusException e) when(e.Reason == ServiceBusFailureReason.MessagingEntityNotFound)
+ {
+ //ignore not found
+ }
+
+ await client.CreateSubscriptionAsync(new CreateSubscriptionOptions(topic, request.Name));
+ return Results.Ok();
+ }
+
+ private static async Task DeleteSubscription(string topic, string sub,
+ ServiceBusAdministrationClient client)
+ {
+ var currentSubscription = await client.GetSubscriptionAsync(topic, sub);
+
+ if (currentSubscription is null)
+ {
+ return Results.Problem(
+ $"Subscription with name '{sub}' does not exist on topic '{topic}'.",
+ statusCode: StatusCodes.Status404NotFound,
+ title: "Not Found");
+ }
+
+ await client.DeleteSubscriptionAsync(topic, sub);
+ return Results.Ok();
+ }
+
+ private static async Task PeekSubscriptionMessages(
+ string topic,
+ string sub,
+ CaseInsensitiveEnum mode,
+ CaseInsensitiveEnum state,
+ ServiceBusEndpointCache endpointCache,
+ int skip = 0,
+ int take = 25)
+ {
+ var options = new ServiceBusReceiverOptions
+ {
+ SubQueue = state.Value switch
+ {
+ MessageState.Deadletter => SubQueue.DeadLetter,
+ _ => SubQueue.None
+ },
+ ReceiveMode = ServiceBusReceiveMode.PeekLock
+ };
+
+ var receiver = endpointCache.GetTopicReceiver(topic, sub, options);
+
+
+ using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+ IReadOnlyList? messages = [];
+ try
+ {
+ messages = await receiver.PeekMessagesAsync(
+ maxMessages: take,
+ fromSequenceNumber: long.MinValue + skip, cancellationToken: cancellationTokenSource.Token);
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+
+ var messageInfos = messages.Select(message => new MessageInfo(
+ message.MessageId,
+ message.Body.ToString().Length <= 50 ? message.Body.ToString() : message.Body.ToString()[..50],
+ message.Body.ToString(),
+ message.EnqueuedTime.UtcDateTime,
+ message.ExpiresAt.UtcDateTime,
+ message.DeliveryCount,
+ message.ContentType,
+ message.SessionId,
+ message.GetRawAmqpMessage().MessageAnnotations.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
+ message.ApplicationProperties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))).ToList();
+
+ var pagedMessages = new PagedMessages(messageInfos, messageInfos.Count, messageInfos.Count != 0);
+
+ return Results.Ok(pagedMessages);
+ }
+}
diff --git a/src/ServiceBusEmulatorExplorer/Endpoints/TopicEndpoints.cs b/src/ServiceBusEmulatorExplorer/Endpoints/TopicEndpoints.cs
new file mode 100644
index 0000000..f85237a
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Endpoints/TopicEndpoints.cs
@@ -0,0 +1,109 @@
+using Azure.Messaging.ServiceBus;
+using Azure.Messaging.ServiceBus.Administration;
+
+namespace ServiceBusEmulatorExplorer.Endpoints;
+
+public static class TopicEndpoints
+{
+ public static IEndpointRouteBuilder MapTopicEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("topics")
+ .WithTags("Topics");
+
+ group.MapGet("/", ListTopics)
+ .WithName("ListTopics")
+ .WithSummary("List topics")
+ .Produces>();
+
+ group.MapPost("/", CreateTopic)
+ .WithName("CreateTopic")
+ .WithSummary("Create topic")
+ .Produces(StatusCodes.Status201Created);
+
+ group.MapDelete("/{name}", DeleteTopic)
+ .WithName("DeleteTopic")
+ .WithSummary("Delete topic")
+ .Produces(StatusCodes.Status200OK);
+
+ group.MapPost("/{topic}/messages", SendTopicMessage)
+ .WithName("SendTopicMessage")
+ .WithSummary("Send message to topic")
+ .Produces(StatusCodes.Status200OK);
+
+ return app;
+ }
+
+ private static async Task ListTopics(ServiceBusAdministrationClient client)
+ {
+ var topicsRuntimeProperties = client.GetTopicsRuntimePropertiesAsync();
+ if (topicsRuntimeProperties is null)
+ {
+ return Results.NotFound();
+ }
+
+ var topics = new List();
+
+ await foreach (var item in topicsRuntimeProperties)
+ {
+
+
+
+ var topicInfo = new TopicInfo(
+ item.Name,
+ EntityStatus.Active,
+ 0,
+ 0
+ );
+
+ topics.Add(topicInfo);
+ }
+
+ return Results.Ok(topics);
+ }
+
+ private static async Task CreateTopic(CreateTopicRequest request, ServiceBusAdministrationClient client) =>
+ await client.CreateTopicAsync(new CreateTopicOptions(request.Name)) switch
+ {
+ { HasValue: true } => Results.Ok(),
+ _ => Results.BadRequest()
+ };
+
+ private static async Task DeleteTopic(string name, ServiceBusAdministrationClient client)
+ {
+ var exists = await client.GetTopicAsync(name);
+ if (exists is null)
+ {
+ return Results.Problem("Topic does not exist", statusCode: StatusCodes.Status400BadRequest, title: "Bad Request");
+ }
+
+ return (await client.DeleteTopicAsync(name)) switch
+ {
+ { IsError: false } => Results.Ok(),
+ _ => Results.Problem("Failed to delete topic", statusCode: StatusCodes.Status500InternalServerError, title: "Internal Server Error")
+ };
+ }
+
+ private static async Task SendTopicMessage(string topic, SendMessageRequest request,
+ ServiceBusAdministrationClient adminClient, ServiceBusClient client)
+ {
+ var exists = await adminClient.GetTopicAsync(topic);
+ if (exists is null)
+ {
+ return Results.Problem("Topic does not exist", statusCode: StatusCodes.Status400BadRequest, title: "Bad Request");
+ }
+
+ var message = new ServiceBusMessage(request.Body)
+ {
+ ContentType = request.ContentType
+ };
+
+ foreach (var (key, value) in request.UserProperties ?? [])
+ {
+ message.ApplicationProperties[key] = value;
+ }
+
+ await using var sender = client.CreateSender(topic);
+ await sender.SendMessageAsync(message);
+ return Results.Ok();
+ }
+}
diff --git a/src/ServiceBusEmulatorExplorer/Extensions/BuilderExtensions.cs b/src/ServiceBusEmulatorExplorer/Extensions/BuilderExtensions.cs
new file mode 100644
index 0000000..ed43427
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Extensions/BuilderExtensions.cs
@@ -0,0 +1,88 @@
+using OpenTelemetry.Exporter;
+using OpenTelemetry.Logs;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Resources;
+using OpenTelemetry.Trace;
+
+namespace ServiceBusEmulatorExplorer.Extensions;
+
+public static class BuilderExtensions
+{
+ public static IHostApplicationBuilder AddOpenTelemetry(this IHostApplicationBuilder builder)
+ {
+ var resourceBuilder = ResourceBuilder
+ .CreateDefault()
+ .AddService(serviceName: builder.Environment.ApplicationName,
+ serviceVersion: typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0")
+ .AddAttributes([new KeyValuePair("deployment.environment", builder.Environment.EnvironmentName)]);
+
+ var otlpConfig = builder.Configuration.GetSection(OtlpConfig.SectionName).Get() ??
+ throw new InvalidOperationException("OtlpConfig section is missing");
+
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.SetResourceBuilder(resourceBuilder);
+ logging.IncludeScopes = true;
+ logging.ParseStateValues = true;
+
+
+ if (!string.IsNullOrEmpty(otlpConfig.Endpoint))
+ {
+ logging.AddOtlpExporter(opts =>
+ {
+ opts.Endpoint = new Uri($"{otlpConfig.Endpoint}/v1/logs");
+ opts.Protocol = OtlpExportProtocol.HttpProtobuf;
+ });
+ }
+
+ if (otlpConfig.EnableConsoleExporter)
+ {
+ logging.AddConsoleExporter();
+ }
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithTracing(tracing =>
+ {
+ tracing.SetResourceBuilder(resourceBuilder);
+ tracing.SetSampler(new ParentBasedSampler(new AlwaysOnSampler()));
+ tracing.AddAspNetCoreInstrumentation();
+ tracing.AddHttpClientInstrumentation();
+
+ if (!string.IsNullOrEmpty(otlpConfig.Endpoint))
+ {
+ tracing.AddOtlpExporter(opts =>
+ {
+ opts.Endpoint = new Uri($"{otlpConfig.Endpoint}/v1/traces");
+ opts.Protocol = OtlpExportProtocol.HttpProtobuf;
+ });
+ }
+
+ if (otlpConfig.EnableConsoleExporter)
+ {
+ tracing.AddConsoleExporter();
+ }
+ })
+ .WithMetrics(metrics =>
+ {
+ metrics.SetResourceBuilder(resourceBuilder);
+ metrics.AddAspNetCoreInstrumentation();
+ metrics.AddRuntimeInstrumentation();
+ if (!string.IsNullOrEmpty(otlpConfig.Endpoint))
+ {
+ metrics.AddOtlpExporter(opts =>
+ {
+ opts.Endpoint = new Uri($"{otlpConfig.Endpoint}/v1/metrics");
+ opts.Protocol = OtlpExportProtocol.HttpProtobuf;
+ });
+ }
+
+ if (otlpConfig.EnableConsoleExporter)
+ {
+ metrics.AddConsoleExporter();
+ }
+ });
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/ServiceBusEmulatorExplorer/Helpers.cs b/src/ServiceBusEmulatorExplorer/Helpers.cs
new file mode 100644
index 0000000..227f5e6
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Helpers.cs
@@ -0,0 +1,22 @@
+namespace ServiceBusEmulatorExplorer;
+
+public class Helpers
+{
+
+}
+
+public readonly record struct CaseInsensitiveEnum(T Value) where T : struct, Enum
+{
+ public static bool TryParse(string? value, out CaseInsensitiveEnum result)
+ {
+ if (Enum.TryParse(value, ignoreCase: true, out var parsed))
+ {
+ result = new CaseInsensitiveEnum(parsed);
+ return true;
+ }
+ result = default;
+ return false;
+ }
+
+ public static implicit operator T(CaseInsensitiveEnum e) => e.Value;
+}
diff --git a/src/ServiceBusEmulatorExplorer/Models.cs b/src/ServiceBusEmulatorExplorer/Models.cs
new file mode 100644
index 0000000..d71d074
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Models.cs
@@ -0,0 +1,104 @@
+using System.Text.Json.Serialization;
+
+namespace ServiceBusEmulatorExplorer;
+
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum EntityStatus
+{
+ Active,
+ Disabled,
+ SendDisabled,
+ ReceiveDisabled
+}
+
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum MessageState
+{
+ Active,
+ Deadletter
+}
+
+[JsonConverter(typeof(JsonStringEnumConverter))]
+public enum PeekMode
+{
+ Peek
+}
+
+public record QueueInfo(
+ string Name,
+ EntityStatus Status,
+ long ActiveMessageCount,
+ long DeadLetterMessageCount,
+ long? ScheduledMessageCount = null,
+ long? MaxDeliveryCount = null,
+ string? LockDuration = null,
+ string? DefaultTtl = null,
+ DateTimeOffset? CreatedAt = null
+);
+
+public record TopicInfo(
+ string Name,
+ EntityStatus Status,
+ int ActiveMessageCount,
+ int DeadLetterMessageCount,
+ int? ScheduledMessageCount = null,
+ DateTime? CreatedAt = null
+);
+
+public record SubscriptionInfo(
+ string Name,
+ EntityStatus Status,
+ long ActiveMessageCount,
+ long DeadLetterMessageCount,
+ long? ScheduledMessageCount = null,
+ long? MaxDeliveryCount = null,
+ string? LockDuration = null,
+ string? DefaultTtl = null
+);
+
+public record MessageInfo(
+ string MessageId,
+ string BodyPreview,
+ string? Body = null,
+ DateTime? EnqueuedTime = null,
+ DateTime? ExpiresAt = null,
+ int? DeliveryCount = null,
+ string? ContentType = null,
+ string? SessionId = null,
+ Dictionary? UserProperties = null,
+ Dictionary? SystemProperties = null
+);
+
+public record PagedMessages(
+ IReadOnlyList Items,
+ int? Total = null,
+ bool? HasMore = null
+);
+
+public record SendMessageRequest(
+ string Body,
+ string? ContentType = null,
+ Dictionary? UserProperties = null
+);
+
+public record CreateQueueRequest(
+ string Name,
+ int? MaxDeliveryCount = null,
+ string? LockDuration = null,
+ string? DefaultTtl = null
+);
+
+public record CreateTopicRequest(
+ string Name
+);
+
+public record CreateSubscriptionRequest(
+ string Name,
+ int? MaxDeliveryCount = null,
+ string? LockDuration = null,
+ string? DefaultTtl = null
+);
+
+public record BulkDlqDeleteRequest(
+ List? MessageIds = null
+);
diff --git a/src/ServiceBusEmulatorExplorer/OtlpConfig.cs b/src/ServiceBusEmulatorExplorer/OtlpConfig.cs
new file mode 100644
index 0000000..d05d2c4
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/OtlpConfig.cs
@@ -0,0 +1,8 @@
+namespace ServiceBusEmulatorExplorer;
+
+public class OtlpConfig
+{
+ public static string SectionName => "Otlp";
+ public string Endpoint { get; set; } = string.Empty;
+ public bool EnableConsoleExporter { get; set; }
+}
\ No newline at end of file
diff --git a/src/ServiceBusEmulatorExplorer/Program.cs b/src/ServiceBusEmulatorExplorer/Program.cs
new file mode 100644
index 0000000..2071a5d
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Program.cs
@@ -0,0 +1,137 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.WebUtilities;
+using Microsoft.Extensions.Azure;
+using Scalar.AspNetCore;
+using ServiceBusEmulatorExplorer.Endpoints;
+using ServiceBusEmulatorExplorer;
+using ServiceBusEmulatorExplorer.Extensions;
+
+#if DEBUG
+var builder = WebApplication.CreateBuilder(args);
+#else
+var builder = WebApplication.CreateSlimBuilder(args);
+#endif
+
+
+builder
+ .Configuration.AddJsonFile("appsettings.json", optional: false)
+ .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true)
+ .AddEnvironmentVariables();
+
+var serviceBusConfig = builder.Configuration.GetSection(ServiceBusConfig.SectionName).Get() ??
+ throw new InvalidOperationException("ServiceBusConfig section is missing");
+
+builder.Services
+ .AddOpenApi()
+ .AddEndpointsApiExplorer()
+ .AddProblemDetails(options => options.CustomizeProblemDetails = context =>
+ {
+ context.ProblemDetails.Extensions["traceId"] = context.HttpContext.TraceIdentifier;
+ })
+ .AddExceptionHandler();
+
+builder.AddOpenTelemetry();
+
+builder.Services.AddHealthChecks();
+
+builder.Services.ConfigureHttpJsonOptions(options =>
+{
+ options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ options.SerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
+ options.SerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
+ options.SerializerOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
+ options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonContext.Default);
+});
+
+
+builder.Services.AddAzureClients(clientBuilder =>
+{
+ clientBuilder.AddServiceBusClient(serviceBusConfig.ConnectionString);
+ clientBuilder.AddServiceBusAdministrationClient(serviceBusConfig.AdministrationConnectionString);
+});
+
+builder.Services.AddSpaStaticFiles(options => { options.RootPath = "wwwroot"; });
+
+builder.Services.AddSingleton();
+
+
+builder.Services.AddCors(options =>
+{
+ options.AddPolicy("AllowFrontend", policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyHeader()
+ .AllowAnyMethod();
+ });
+});
+
+
+var app = builder.Build();
+app.UseDefaultFiles();
+app.UseStaticFiles();
+if (!app.Environment.IsDevelopment())
+{
+ app.UseSpaStaticFiles();
+}
+
+app.UseCors("AllowFrontend");
+app.MapOpenApi();
+app.MapScalarApiReference();
+
+app.UseExceptionHandler();
+app.UseStatusCodePages(async statusCodeContext =>
+{
+ var httpContext = statusCodeContext.HttpContext;
+ if (httpContext.Response.HasStarted)
+ {
+ return;
+ }
+
+ var problemDetailsService = httpContext.RequestServices.GetRequiredService();
+ var statusCode = httpContext.Response.StatusCode;
+ var problemDetails = new ProblemDetails
+ {
+ Status = statusCode,
+ Title = ReasonPhrases.GetReasonPhrase(statusCode)
+ };
+
+ await problemDetailsService.WriteAsync(new ProblemDetailsContext
+ {
+ HttpContext = httpContext,
+ ProblemDetails = problemDetails
+ });
+});
+
+app.UseHttpsRedirection();
+
+app.MapGroup("/api")
+ .MapQueueEndpoints()
+ .MapTopicEndpoints()
+ .MapSubscriptionEndpoints()
+ .MapDeadLetterEndpoints();
+
+app.UseHealthChecks("/health");
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseWhen(context =>
+ !context.Request.Path.StartsWithSegments("/api")
+ && !context.Request.Path.StartsWithSegments("/scalar")
+ && !context.Request.Path.StartsWithSegments("/openapi"), spaApp =>
+ {
+ spaApp.UseSpa(spa =>
+ {
+ spa.Options.SourcePath = "../app/sb-explorer-ui";
+ var proxyUrl = builder.Configuration["SpaProxyServerUrl"] ?? "http://localhost:5173";
+ spa.UseProxyToSpaDevelopmentServer(proxyUrl);
+ });
+ });
+}
+else
+{
+ app.MapFallbackToFile("index.html");
+}
+
+app.Run();
\ No newline at end of file
diff --git a/src/ServiceBusEmulatorExplorer/Properties/launchSettings.json b/src/ServiceBusEmulatorExplorer/Properties/launchSettings.json
new file mode 100644
index 0000000..526b0a6
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/Properties/launchSettings.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5123",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:7229;http://localhost:5123",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
+ }
+ }
+ }
+}
diff --git a/src/ServiceBusEmulatorExplorer/ServiceBusConfig.cs b/src/ServiceBusEmulatorExplorer/ServiceBusConfig.cs
new file mode 100644
index 0000000..ba42f3b
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/ServiceBusConfig.cs
@@ -0,0 +1,9 @@
+namespace ServiceBusEmulatorExplorer;
+
+public class ServiceBusConfig
+{
+ public const string SectionName = "ServiceBus";
+ public string ConnectionString { get; set; } = null!;
+ public string AdministrationConnectionString { get; set; } = null!;
+ public string? EmulatorConfigFilePath { get; set; }
+}
\ No newline at end of file
diff --git a/src/ServiceBusEmulatorExplorer/ServiceBusEmulatorExplorer.csproj b/src/ServiceBusEmulatorExplorer/ServiceBusEmulatorExplorer.csproj
new file mode 100644
index 0000000..717d379
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/ServiceBusEmulatorExplorer.csproj
@@ -0,0 +1,82 @@
+
+
+
+ net10.0
+ enable
+ enable
+ ServiceBusEmulatorExplorer
+ Linux
+ latest
+ ServiceBusEmulatorExplorer.Api
+ ../../app/sb-explorer-ui/
+ http://localhost:5173
+ npm run dev
+ true
+ true
+ link
+ true
+
+
+ 0.0.0
+ $(Version)
+
+
+
+ true
+ linux-musl-x64
+ none
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ .dockerignore
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ wwwroot/%(RecursiveDir)%(FileName)%(Extension)
+ PreserveNewest
+ true
+
+
+
+
+
diff --git a/src/ServiceBusEmulatorExplorer/ServiceBusEndpointCache.cs b/src/ServiceBusEmulatorExplorer/ServiceBusEndpointCache.cs
new file mode 100644
index 0000000..95abc97
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/ServiceBusEndpointCache.cs
@@ -0,0 +1,37 @@
+using System.Collections.Concurrent;
+using Azure.Messaging.ServiceBus;
+
+namespace ServiceBusEmulatorExplorer;
+
+public sealed class ServiceBusEndpointCache(ServiceBusClient client) : IAsyncDisposable
+{
+ private readonly ConcurrentDictionary _senders = new();
+ private readonly ConcurrentDictionary _receivers = new();
+
+ public ServiceBusSender GetSender(string queue) =>
+ _senders.GetOrAdd(queue, client.CreateSender);
+
+ public ServiceBusReceiver GetReceiver(string queue, ServiceBusReceiverOptions receiverOptions)
+ {
+ var key = $"{queue}-{receiverOptions.ReceiveMode}-{receiverOptions.SubQueue}";
+ return _receivers.GetOrAdd(key, _ => client.CreateReceiver(queue, receiverOptions));
+ }
+
+ public ServiceBusReceiver GetTopicReceiver(
+ string topic,
+ string subscription,
+ ServiceBusReceiverOptions receiverOptions)
+ {
+ var entityPath = $"{topic}/Subscriptions/{subscription}";
+ var key = $"{entityPath}-{receiverOptions.ReceiveMode}-{receiverOptions.SubQueue}";
+ return _receivers.GetOrAdd(key, _ => client.CreateReceiver(topic, subscription, receiverOptions));
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ foreach (var sender in _senders.Values)
+ await sender.DisposeAsync();
+ foreach (var receiver in _receivers.Values)
+ await receiver.DisposeAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/ServiceBusEmulatorExplorer/ServiceBusExceptionHandler.cs b/src/ServiceBusEmulatorExplorer/ServiceBusExceptionHandler.cs
new file mode 100644
index 0000000..4aa4e52
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/ServiceBusExceptionHandler.cs
@@ -0,0 +1,135 @@
+using System.Text.RegularExpressions;
+using Azure;
+using Azure.Messaging.ServiceBus;
+using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.WebUtilities;
+
+namespace ServiceBusEmulatorExplorer;
+
+public sealed class ServiceBusExceptionHandler : IExceptionHandler
+{
+ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
+ {
+ if (httpContext.Response.HasStarted)
+ {
+ return false;
+ }
+
+ if (exception is ServiceBusException serviceBusException)
+ {
+ var (statusCode, title, detail) = MapServiceBusFailure(serviceBusException);
+ var problemDetails = new ProblemDetails
+ {
+ Status = statusCode,
+ Title = title,
+ Detail = detail
+ };
+
+ await WriteProblemDetailsAsync(httpContext, problemDetails, exception);
+ return true;
+ }
+
+ if (exception is RequestFailedException requestFailedException)
+ {
+ var statusCode = requestFailedException.Status;
+ var title = ReasonPhrases.GetReasonPhrase(statusCode);
+ var problemDetails = new ProblemDetails
+ {
+ Status = statusCode,
+ Title = string.IsNullOrWhiteSpace(title) ? "Bad Request" : title,
+ Detail = FormatServiceBusDetail(requestFailedException.Message)
+ };
+
+ await WriteProblemDetailsAsync(httpContext, problemDetails, exception);
+ return true;
+ }
+
+ if (exception is ArgumentException argumentException)
+ {
+ var problemDetails = new ProblemDetails
+ {
+ Status = StatusCodes.Status400BadRequest,
+ Title = "Bad Request",
+ Detail = FormatServiceBusDetail(argumentException.Message)
+ };
+
+ await WriteProblemDetailsAsync(httpContext, problemDetails, exception);
+ return true;
+ }
+
+ return false;
+ }
+
+ private static (int statusCode, string title, string detail) MapServiceBusFailure(ServiceBusException exception)
+ {
+ var (statusCode, title) = exception.Reason switch
+ {
+ ServiceBusFailureReason.MessagingEntityAlreadyExists => (StatusCodes.Status409Conflict, "Conflict"),
+ ServiceBusFailureReason.MessagingEntityNotFound => (StatusCodes.Status404NotFound, "Not Found"),
+ ServiceBusFailureReason.MessagingEntityDisabled => (StatusCodes.Status409Conflict, "Conflict"),
+ ServiceBusFailureReason.MessageSizeExceeded => (StatusCodes.Status413PayloadTooLarge, "Payload Too Large"),
+ ServiceBusFailureReason.QuotaExceeded => (StatusCodes.Status429TooManyRequests, "Quota Exceeded"),
+ ServiceBusFailureReason.ServiceBusy => (StatusCodes.Status503ServiceUnavailable, "Service Unavailable"),
+ ServiceBusFailureReason.ServiceTimeout => (StatusCodes.Status504GatewayTimeout, "Gateway Timeout"),
+
+ _ => (StatusCodes.Status400BadRequest, exception.Reason.ToString()),
+ };
+
+ var detail = exception.Reason switch
+ {
+ ServiceBusFailureReason.MessagingEntityAlreadyExists => "Entity already exists.",
+ ServiceBusFailureReason.MessagingEntityNotFound => "Entity not found.",
+ ServiceBusFailureReason.MessagingEntityDisabled => "Entity is disabled.",
+ ServiceBusFailureReason.MessageSizeExceeded => "Message size exceeds the allowed limit.",
+ ServiceBusFailureReason.QuotaExceeded => "The Service Bus quota has been exceeded.",
+ ServiceBusFailureReason.ServiceBusy => "Service Bus is busy. Try again later.",
+ ServiceBusFailureReason.ServiceTimeout => "Service Bus timed out. Try again.",
+ _ => FormatServiceBusDetail(exception.Message)
+ };
+
+ return (statusCode, title, detail);
+ }
+
+ private static string FormatServiceBusDetail(string message)
+ {
+ if (string.IsNullOrWhiteSpace(message))
+ {
+ return "Service Bus request failed.";
+ }
+
+ var detailMatch = Regex.Match(message, "(?.*) ", RegexOptions.Singleline);
+ var detail = detailMatch.Success ? detailMatch.Groups["detail"].Value : message;
+ detail = Regex.Replace(detail, "SubCode=\\d+\\.?\\s*", string.Empty, RegexOptions.IgnoreCase);
+ detail = StripDetailSuffix(detail, "TrackingId:");
+ detail = StripDetailSuffix(detail, "SystemTracker:");
+ detail = StripDetailSuffix(detail, "Timestamp:");
+ detail = StripDetailSuffix(detail, "Content:");
+ detail = detail.Trim();
+ if (detail.Length <= 220)
+ {
+ return detail;
+ }
+
+ return detail[..217] + "...";
+ }
+
+ private static string StripDetailSuffix(string detail, string token)
+ {
+ var index = detail.IndexOf(token, StringComparison.OrdinalIgnoreCase);
+ return index >= 0 ? detail[..index].Trim() : detail;
+ }
+
+ private static async Task WriteProblemDetailsAsync(HttpContext httpContext, ProblemDetails problemDetails, Exception exception)
+ {
+ httpContext.Response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;
+
+ var problemDetailsService = httpContext.RequestServices.GetRequiredService();
+ await problemDetailsService.WriteAsync(new ProblemDetailsContext
+ {
+ HttpContext = httpContext,
+ ProblemDetails = problemDetails,
+ Exception = exception
+ });
+ }
+}
diff --git a/appsettings.Development.json b/src/ServiceBusEmulatorExplorer/appsettings.Development.json
similarity index 100%
rename from appsettings.Development.json
rename to src/ServiceBusEmulatorExplorer/appsettings.Development.json
diff --git a/appsettings.json b/src/ServiceBusEmulatorExplorer/appsettings.json
similarity index 61%
rename from appsettings.json
rename to src/ServiceBusEmulatorExplorer/appsettings.json
index e77f7b1..13f1eca 100644
--- a/appsettings.json
+++ b/src/ServiceBusEmulatorExplorer/appsettings.json
@@ -6,13 +6,14 @@
}
},
"AllowedHosts": "*",
+ "Otlp": {
+ "Endpoint": "https://otel.k8s.pasturegrove.dev",
+ "Headers": ""
+ },
"ServiceBus": {
"ConnectionString": "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
+ "AdministrationConnectionString": "Endpoint=sb://localhost:5300;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;",
"RefreshIntervalMs": 5000,
"EmulatorConfigFilePath": "service-bus-config.json"
- },
- "Redis": {
- "ConnectionString": "localhost:6379,password=pass@w0rd1",
- "InstanceName": "SbExplorer"
}
}
diff --git a/src/ServiceBusEmulatorExplorer/sb-explorer.http b/src/ServiceBusEmulatorExplorer/sb-explorer.http
new file mode 100644
index 0000000..98e6404
--- /dev/null
+++ b/src/ServiceBusEmulatorExplorer/sb-explorer.http
@@ -0,0 +1,6 @@
+@sb_explorer_HostAddress = http://localhost:5123
+
+GET {{sb_explorer_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/test/ServiceBusEmulatorExplorer.Tests/GlobalSetup.cs b/test/ServiceBusEmulatorExplorer.Tests/GlobalSetup.cs
new file mode 100644
index 0000000..a501228
--- /dev/null
+++ b/test/ServiceBusEmulatorExplorer.Tests/GlobalSetup.cs
@@ -0,0 +1,22 @@
+// Here you could define global logic that would affect all tests
+
+// You can use attributes at the assembly level to apply to all tests in the assembly
+[assembly: Retry(3)]
+[assembly: System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+
+namespace ServiceBusEmulatorExplorer.Tests;
+
+public class GlobalHooks
+{
+ [Before(TestSession)]
+ public static void SetUp()
+ {
+ Console.WriteLine(@"Or you can define methods that do stuff before...");
+ }
+
+ [After(TestSession)]
+ public static void CleanUp()
+ {
+ Console.WriteLine(@"...and after!");
+ }
+}
diff --git a/test/ServiceBusEmulatorExplorer.Tests/ServiceBusEmulatorExplorer.Tests.csproj b/test/ServiceBusEmulatorExplorer.Tests/ServiceBusEmulatorExplorer.Tests.csproj
new file mode 100644
index 0000000..2db2704
--- /dev/null
+++ b/test/ServiceBusEmulatorExplorer.Tests/ServiceBusEmulatorExplorer.Tests.csproj
@@ -0,0 +1,19 @@
+
+
+
+ enable
+ enable
+ Exe
+ net10.0
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/ServiceBusEmulatorExplorer.Tests/TestServiceBusClient.cs b/test/ServiceBusEmulatorExplorer.Tests/TestServiceBusClient.cs
new file mode 100644
index 0000000..e234f11
--- /dev/null
+++ b/test/ServiceBusEmulatorExplorer.Tests/TestServiceBusClient.cs
@@ -0,0 +1,318 @@
+using System.Diagnostics.CodeAnalysis;
+using Azure;
+using Azure.Core;
+using Azure.Messaging.ServiceBus;
+using Azure.Messaging.ServiceBus.Administration;
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+
+namespace ServiceBusEmulatorExplorer.Tests;
+
+[SuppressMessage("Compiler", "CS8625:Cannot convert null literal to non-nullable reference type.")]
+public class TestServiceBusClient : ServiceBusClient
+{
+ private readonly Dictionary _queueReceivers = [];
+ private readonly Dictionary _topicReceivers = [];
+ private readonly Dictionary _senders = [];
+
+
+ public override ServiceBusSender CreateSender(string queueOrTopicName)
+ {
+ var sender = new TestServiceBusSender(queueOrTopicName);
+ _senders[queueOrTopicName] = sender;
+
+ return sender;
+ }
+
+ public override ServiceBusSender CreateSender(string queueOrTopicName, ServiceBusSenderOptions options) => CreateSender(queueOrTopicName);
+
+ public override ServiceBusReceiver CreateReceiver(string queueName,
+ ServiceBusReceiverOptions receiverOptions = null)
+ {
+ var receiver = new TestServiceBusReceiver(queueName, this);
+ _queueReceivers[queueName] = receiver;
+
+ return receiver;
+ }
+
+ public override ServiceBusReceiver CreateReceiver(string topicName, string subscriptionName,
+ ServiceBusReceiverOptions options)
+ {
+ var key = $"{topicName}/Subscriptions/{subscriptionName}";
+ var receiver = new TestServiceBusReceiver(key, this);
+ _topicReceivers[key] = receiver;
+
+ return receiver;
+ }
+
+
+ private class TestServiceBusSender(string entityPath) : ServiceBusSender
+ {
+ public override string EntityPath => entityPath;
+
+ internal List Messages { get; } = [];
+
+
+ public override Task SendMessageAsync(ServiceBusMessage message, CancellationToken cancellationToken = new())
+ {
+ Messages.Add(message);
+ return Task.CompletedTask;
+ }
+ }
+
+ public class TestServiceBusReceiver(string entityPath, TestServiceBusClient client) : ServiceBusReceiver
+ {
+ public override string EntityPath => entityPath;
+
+
+ public override Task> PeekMessagesAsync(int maxMessages, long? fromSequenceNumber = null,
+ CancellationToken cancellationToken = new())
+ {
+ var sender = client._senders.GetValueOrDefault(entityPath) ?? throw new InvalidOperationException($"No sender found for entity path '{entityPath}'");
+
+ var messages = sender.Messages
+ .Skip((int)(fromSequenceNumber ?? 0))
+ .Take(maxMessages)
+ .Select(m => ServiceBusModelFactory.ServiceBusReceivedMessage(
+ body: m.Body,
+ messageId: m.MessageId,
+ sequenceNumber: sender.Messages.IndexOf(m) + 1))
+ .ToList()
+ .AsReadOnly();
+
+ return Task.FromResult>(messages);
+
+ }
+ }
+}
+
+public class TestServiceBusAdministrationClient : ServiceBusAdministrationClient
+{
+ private readonly Dictionary _queues = [];
+ private readonly Dictionary _topics = [];
+ private readonly Dictionary _subscriptions = [];
+
+ public override Task DeleteQueueAsync(string name, CancellationToken cancellationToken = new())
+ {
+ if (_queues.GetValueOrDefault(name) is not null)
+ {
+ _queues.Remove(name);
+ return Task.FromResult(new TestResponse(200));
+ }
+
+ return Task.FromResult(new TestResponse(404));
+ }
+
+ public override Task> GetQueueAsync(string name, CancellationToken cancellationToken = new())
+ {
+ if (_queues.GetValueOrDefault(name) is null)
+ {
+ return Task.FromResult(Response.FromValue(null!, new TestResponse(404)));
+ }
+
+ var queueProperties = ServiceBusModelFactory.QueueProperties(
+ name: name,
+ lockDuration: TimeSpan.FromMinutes(1),
+ maxDeliveryCount: 10,
+ defaultMessageTimeToLive: TimeSpan.FromDays(14),
+ autoDeleteOnIdle: TimeSpan.MaxValue,
+ duplicateDetectionHistoryTimeWindow: TimeSpan.FromDays(1),
+ userMetadata:"");
+
+ return Task.FromResult(Response.FromValue(queueProperties, new TestResponse(200)));
+ }
+
+ public override Task> CreateQueueAsync(string name, CancellationToken cancellationToken = new()) => CreateQueueAsync(new CreateQueueOptions(name), cancellationToken);
+
+ public override Task> CreateQueueAsync(CreateQueueOptions options, CancellationToken cancellationToken = new())
+ {
+ if (_queues.GetValueOrDefault(options.Name) is not null)
+ {
+ return Task.FromResult(Response.FromValue(null!, new TestResponse(409)));
+ }
+
+ var queueProperties = ServiceBusModelFactory.QueueProperties(
+ name: options.Name,
+ lockDuration: options.LockDuration,
+ maxDeliveryCount: options.MaxDeliveryCount,
+ defaultMessageTimeToLive: options.DefaultMessageTimeToLive,
+ autoDeleteOnIdle: TimeSpan.MaxValue,
+ duplicateDetectionHistoryTimeWindow: TimeSpan.FromDays(1),
+ userMetadata:"");
+
+ _queues[options.Name] = options;
+
+ return Task.FromResult(Response.FromValue(queueProperties, new TestResponse(201)));
+ }
+
+ public override AsyncPageable GetQueuesRuntimePropertiesAsync(CancellationToken cancellationToken = new()) =>
+ AsyncPageable.FromPages(
+ [
+ Page.FromValues(
+ _queues.Select(q => ServiceBusModelFactory.QueueRuntimeProperties(
+ name: q.Key,
+ activeMessageCount: 0,
+ deadLetterMessageCount: 0,
+ scheduledMessageCount: 0,
+ transferMessageCount: 0,
+ transferDeadLetterMessageCount: 0,
+ sizeInBytes: 0,
+ createdAt: DateTimeOffset.UtcNow,
+ updatedAt: DateTimeOffset.UtcNow,
+ accessedAt: DateTimeOffset.UtcNow)).ToList(),
+ continuationToken: null,
+ response: new TestResponse(200))
+ ]);
+
+
+ public override Task> CreateSubscriptionAsync(CreateSubscriptionOptions options,
+ CancellationToken cancellationToken = new())
+ {
+ if(!_topics.ContainsKey(options.TopicName))
+ {
+ return Task.FromResult(
+ Response.FromValue(null!, new TestResponse(404)));
+ }
+
+ var subscriptionProperties = ServiceBusModelFactory.SubscriptionProperties(
+ topicName: options.TopicName,
+ subscriptionName: options.SubscriptionName,
+ lockDuration: options.LockDuration,
+ defaultMessageTimeToLive: options.DefaultMessageTimeToLive,
+ autoDeleteOnIdle: TimeSpan.MaxValue);
+
+ _subscriptions[$"{options.TopicName}/Subscriptions/{options.SubscriptionName}"] = options;
+ return Task.FromResult(Response.FromValue(subscriptionProperties, new TestResponse(201)));
+ }
+
+ public override Task DeleteSubscriptionAsync(string topicName, string subscriptionName,
+ CancellationToken cancellationToken = new())
+ {
+ var key = $"{topicName}/Subscriptions/{subscriptionName}";
+ if (_subscriptions.GetValueOrDefault(key) is not null)
+ {
+ _subscriptions.Remove(key);
+ return Task.FromResult(new TestResponse(200));
+ }
+
+ return Task.FromResult(new TestResponse(404));
+ }
+
+ public override Task> GetSubscriptionAsync(string topicName, string subscriptionName,
+ CancellationToken cancellationToken = new())
+ {
+ var key = $"{topicName}/Subscriptions/{subscriptionName}";
+ if (_subscriptions.GetValueOrDefault(key) is null)
+ {
+ return Task.FromResult(
+ Response.FromValue(null!, new TestResponse(404)));
+ }
+
+ var subscriptionProperties = ServiceBusModelFactory.SubscriptionProperties(
+ topicName: topicName,
+ subscriptionName: subscriptionName,
+ lockDuration: TimeSpan.FromMinutes(1),
+ defaultMessageTimeToLive: TimeSpan.FromDays(14),
+ autoDeleteOnIdle: TimeSpan.MaxValue);
+
+ return Task.FromResult(Response.FromValue(subscriptionProperties, new TestResponse(200)));
+ }
+
+ public override AsyncPageable GetSubscriptionsRuntimePropertiesAsync(string topicName,
+ CancellationToken cancellationToken = new())
+ {
+ throw new NotImplementedException();
+ }
+
+ public override AsyncPageable GetTopicsRuntimePropertiesAsync(CancellationToken cancellationToken = new()) =>
+ AsyncPageable.FromPages(
+ [
+ Page.FromValues(
+ _topics.Select(t => ServiceBusModelFactory.TopicRuntimeProperties(
+ name: t.Key,
+ sizeInBytes: 0,
+ createdAt: DateTimeOffset.UtcNow,
+ updatedAt: DateTimeOffset.UtcNow,
+ accessedAt: DateTimeOffset.UtcNow)).ToList(),
+ continuationToken: null,
+ response: new TestResponse(200))
+ ]);
+
+ public override Task> CreateTopicAsync(CreateTopicOptions options, CancellationToken cancellationToken = new())
+ {
+ if (_topics.GetValueOrDefault(options.Name) is not null)
+ {
+ return Task.FromResult(
+ Response.FromValue(null!, new TestResponse(409)));
+ }
+
+ var topicProperties = ServiceBusModelFactory.TopicProperties(
+ name: options.Name,
+ defaultMessageTimeToLive: options.DefaultMessageTimeToLive,
+ autoDeleteOnIdle: TimeSpan.MaxValue,
+ duplicateDetectionHistoryTimeWindow: TimeSpan.FromDays(1));
+ _topics[options.Name] = options;
+ return Task.FromResult(Response.FromValue(topicProperties, new TestResponse(201)));
+ }
+
+ public override Task DeleteTopicAsync(string name, CancellationToken cancellationToken = new())
+ {
+ if (_topics.GetValueOrDefault(name) is not null)
+ {
+ _topics.Remove(name);
+ return Task.FromResult(new TestResponse(200));
+ }
+
+ return Task.FromResult(new TestResponse(404));
+ }
+
+ public override Task> GetTopicAsync(string name, CancellationToken cancellationToken = new())
+ {
+ if (_topics.GetValueOrDefault(name) is null)
+ {
+ return Task.FromResult(
+ Response.FromValue(null!, new TestResponse(404)));
+ }
+
+ var topicProperties = ServiceBusModelFactory.TopicProperties(
+ name: name,
+ defaultMessageTimeToLive: TimeSpan.FromDays(14),
+ autoDeleteOnIdle: TimeSpan.MaxValue,
+ duplicateDetectionHistoryTimeWindow: TimeSpan.FromDays(1));
+
+ return Task.FromResult(Response.FromValue(topicProperties, new TestResponse(200)));
+ }
+
+ private class TestResponse(int status) : Response
+ {
+ public override int Status => status;
+
+ public override string ReasonPhrase => throw new NotImplementedException();
+
+ public override Stream? ContentStream { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ public override string ClientRequestId { get; set; } = Guid.NewGuid().ToString();
+
+ public override void Dispose()
+ {
+ }
+
+ protected override bool TryGetHeader(string name, [NotNullWhen(true)] out string? value)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override bool TryGetHeaderValues(string name, [NotNullWhen(true)] out IEnumerable? values)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override bool ContainsHeader(string name)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override IEnumerable EnumerateHeaders()
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/ServiceBusEmulatorExplorer.Tests/Tests.cs b/test/ServiceBusEmulatorExplorer.Tests/Tests.cs
new file mode 100644
index 0000000..1bc5779
--- /dev/null
+++ b/test/ServiceBusEmulatorExplorer.Tests/Tests.cs
@@ -0,0 +1,61 @@
+using System.Net;
+using System.Net.Http.Json;
+
+namespace ServiceBusEmulatorExplorer.Tests;
+
+public class Tests : TestBase
+{
+ [Test]
+ public async Task Basic()
+ {
+ var client = Factory.CreateClient();
+ var response = await client.GetAsync("/health");
+
+ await Assert.That(response.IsSuccessStatusCode).IsEqualTo(true);
+ }
+
+ [Test]
+ public async Task QueueCrudOperations()
+ {
+ var client = Factory.CreateClient();
+ var response = await client.PostAsync("/api/queues", JsonContent.Create(new { name = "test-queue" }));
+
+ await Assert.That(response.IsSuccessStatusCode).IsEqualTo(true);
+
+ var getResponse = await client.GetAsync("/api/queues/");
+ await Assert.That(getResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
+
+ var queues = await getResponse.Content.ReadFromJsonAsync>();
+
+ await Assert.That(queues).Contains(x => x.Name == "test-queue");
+
+ var deleteResponse = await client.DeleteAsync("/api/queues/test-queue");
+ await Assert.That(deleteResponse.IsSuccessStatusCode).IsEqualTo(true);
+
+ var getAfterDeleteResponse = await client.GetAsync("/api/queues/");
+ await Assert.That(getAfterDeleteResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
+
+ var queuesAfterDelete = await getAfterDeleteResponse.Content.ReadFromJsonAsync>();
+ await Assert.That(queuesAfterDelete).DoesNotContain(x => x.Name == "test-queue");
+ }
+
+ [Test]
+ public async Task TopicCrudOperations()
+ {
+ var client = Factory.CreateClient();
+ var response = await client.PostAsync("/api/topics", JsonContent.Create(new { name = "test-topic" }));
+ await Assert.That(response.IsSuccessStatusCode).IsEqualTo(true);
+
+ var getResponse = await client.GetAsync("/api/topics/");
+ await Assert.That(getResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
+
+ var topics = await getResponse.Content.ReadFromJsonAsync>();
+ await Assert.That(topics).Contains(x => x.Name == "test-topic");
+ var deleteResponse = await client.DeleteAsync("/api/topics/test-topic");
+ await Assert.That(deleteResponse.IsSuccessStatusCode).IsEqualTo(true);
+ var getAfterDeleteResponse = await client.GetAsync("/api/topics/");
+ await Assert.That(getAfterDeleteResponse.StatusCode).IsEqualTo(HttpStatusCode.OK);
+ var topicsAfterDelete = await getAfterDeleteResponse.Content.ReadFromJsonAsync>();
+ await Assert.That(topicsAfterDelete).DoesNotContain(x => x.Name == "test-topic");
+ }
+}
\ No newline at end of file
diff --git a/test/ServiceBusEmulatorExplorer.Tests/WebApplicationFactory.cs b/test/ServiceBusEmulatorExplorer.Tests/WebApplicationFactory.cs
new file mode 100644
index 0000000..4a4a10b
--- /dev/null
+++ b/test/ServiceBusEmulatorExplorer.Tests/WebApplicationFactory.cs
@@ -0,0 +1,22 @@
+using Azure.Messaging.ServiceBus;
+using Azure.Messaging.ServiceBus.Administration;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using TUnit.AspNetCore;
+
+namespace ServiceBusEmulatorExplorer.Tests;
+
+public class WebApplicationFactory : TestWebApplicationFactory
+{
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
+ {
+ builder.ConfigureTestServices(services =>
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ });
+ }
+}
+
+public abstract class TestBase : WebApplicationTest;
\ No newline at end of file
diff --git a/wwwroot/favicon.ico b/wwwroot/favicon.ico
deleted file mode 100644
index 1239223..0000000
Binary files a/wwwroot/favicon.ico and /dev/null differ