Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,32 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0-alpine AS build
RUN apk add --no-cache clang binutils musl-dev build-base zlib-dev zlib-static
WORKDIR /src

# Copy props/solution files first for layer caching
# Copy props/project files first for layer caching
COPY Directory.Build.props ./
COPY MicroStack.slnx ./
COPY src/MicroStack/MicroStack.csproj src/MicroStack/
COPY src/MicroStack.UI/MicroStack.UI.csproj src/MicroStack.UI/
COPY src/MicroStack.UI.Client/MicroStack.UI.Client.csproj src/MicroStack.UI.Client/

RUN dotnet restore src/MicroStack/MicroStack.csproj -r linux-musl-x64
RUN dotnet restore src/MicroStack.UI/MicroStack.UI.csproj -r linux-musl-x64

# Copy source and publish
COPY src/MicroStack/ src/MicroStack/
RUN dotnet publish src/MicroStack/MicroStack.csproj -c Release -r linux-musl-x64 -o /app
# Copy source and publish both applications
COPY src/ src/
RUN dotnet publish src/MicroStack/MicroStack.csproj -c Release -r linux-musl-x64 -o /out/microstack
RUN dotnet publish src/MicroStack.UI/MicroStack.UI.csproj -c Release -r linux-musl-x64 --self-contained true -o /out/ui

FROM alpine:3.21
RUN apk add --no-cache libstdc++ libgcc
COPY --from=build /app /app
WORKDIR /app
COPY --from=build /out/microstack/ /app/
COPY --from=build /out/ui/ /app/
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENV ASPNETCORE_HTTP_PORTS=4566
ENV MICROSTACK_UI_PORT=4567
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
EXPOSE 4566
ENTRYPOINT ["/app/MicroStack"]
EXPOSE 4567
ENTRYPOINT ["/entrypoint.sh"]
2 changes: 2 additions & 0 deletions MicroStack.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<Project Path="src/MicroStack/MicroStack.csproj" />
<Project Path="src/MicroStack.Aspire.Hosting/MicroStack.Aspire.Hosting.csproj" />
<Project Path="src/MicroStack.LambdaBootstrap/MicroStack.LambdaBootstrap.csproj" />
<Project Path="src/MicroStack.UI.Client/MicroStack.UI.Client.csproj" />
<Project Path="src/MicroStack.UI/MicroStack.UI.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/MicroStack.Tests/MicroStack.Tests.csproj" />
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ dotnet run --project src/MicroStack/MicroStack.csproj
curl http://localhost:4566/_microstack/health
```

## Admin UI

MicroStack now includes a web UI dashboard on a separate port:

- **API endpoint:** `http://localhost:4566`
- **UI endpoint:** `http://localhost:4567`

The UI includes:

- **Dashboard** — service health and resource counts
- **Request Log** — recent AWS API calls (service, action, account, status, duration)
- **Resource Explorer** — SQS/S3/DynamoDB resources with names/ARNs and attributes

## Usage

Point any AWS SDK client at `http://localhost:4566`:
Expand Down Expand Up @@ -154,6 +167,7 @@ services:
image: ghcr.io/damianh/microstack:latest
ports:
- "4566:4566"
- "4567:4567"
environment:
- PERSIST_STATE=1
volumes:
Expand Down
13 changes: 13 additions & 0 deletions entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh
set -eu

/app/MicroStack &
microstack_pid="$!"

cleanup() {
kill "$microstack_pid" 2>/dev/null || true
}

trap cleanup INT TERM

/app/MicroStack.UI
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting" Version="13.2.2" />
<PackageReference Include="Aspire.Hosting" Version="13.4.6" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Amazon.Lambda.Core" Version="2.*" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="2.*" />
<PackageReference Include="Amazon.Lambda.Core" Version="3.1.1" />
<PackageReference Include="Amazon.Lambda.Serialization.SystemTextJson" Version="3.0.0" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions src/MicroStack.UI.Client/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(Pages.NotFound)">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
16 changes: 16 additions & 0 deletions src/MicroStack.UI.Client/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>

<main>
<div class="top-row px-4">
<span class="text-muted">MicroStack admin dashboard</span>
</div>

<article class="content px-4">
@Body
</article>
</main>
</div>
77 changes: 77 additions & 0 deletions src/MicroStack.UI.Client/Layout/MainLayout.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}

main {
flex: 1;
}

.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}

.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}

.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}

.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}

.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}

@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}

.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}

@media (min-width: 641px) {
.page {
flex-direction: row;
}

.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}

.top-row {
position: sticky;
top: 0;
z-index: 1;
}

.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}

.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
39 changes: 39 additions & 0 deletions src/MicroStack.UI.Client/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">MicroStack UI</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>

<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="nav flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-speedometer2" aria-hidden="true"></span> Dashboard
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="requests">
<span class="bi bi-journal-text" aria-hidden="true"></span> Request Log
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="resources">
<span class="bi bi-diagram-3" aria-hidden="true"></span> Resource Explorer
</NavLink>
</div>
</nav>
</div>

@code {
private bool collapseNavMenu = true;

private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;

private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
83 changes: 83 additions & 0 deletions src/MicroStack.UI.Client/Layout/NavMenu.razor.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}

.top-row {
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}

.navbar-brand {
font-size: 1.1rem;
}

.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}

.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}

.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}

.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}

.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}

.nav-item:first-of-type {
padding-top: 1rem;
}

.nav-item:last-of-type {
padding-bottom: 1rem;
}

.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}

.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}

.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}

@media (min-width: 641px) {
.navbar-toggler {
display: none;
}

.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}

.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
15 changes: 15 additions & 0 deletions src/MicroStack.UI.Client/MicroStack.UI.Client.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
</ItemGroup>

</Project>
18 changes: 18 additions & 0 deletions src/MicroStack.UI.Client/Pages/Counter.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
private int currentCount = 0;

private void IncrementCount()
{
currentCount++;
}
}
Loading
Loading