Skip to content
IzzyBlazor Demos · latest

Enter all results ↑ ↓ move Esc close

Activity log

An admin's activity log from an API that pages by cursor, with no total: the next page loads as you scroll, and the column filters go to the API with the cursor.

Activity
ActivityLog.razor
@inject NavigationManager Navigation

@* An activity log from an API that pages by cursor, with no total: the next page loads as the last
   row comes into view, the Who and Action filters go to the API with the cursor, and a new filter
   starts the list over. The page fits the window: the table scrolls in its own box (MaxHeight, the
   window less the header and title), so its header row, range and Load more stay in view. On
   static pages and with JavaScript off, Load more is a link to the next page. *@
<AppShell Variant="ShellVariant.Canvas">
    <AppHeader>
        <AppBrand Href="@Navigation.GetUriWithQueryParameter("view", (string?)null)" Icon="Lucide.ShieldCheck" Label="Acme Admin" />
        <Box Grow />
        <Avatar Name="Ada Lovelace" />
    </AppHeader>

    <AppMain>
        <Stack Gap="6" Padding="new() { Base = 4, Md = 8 }">
            <Row Gap="3" Align="Align.Center" Wrap>
                <Stack Gap="1" Grow>
                    <Heading Level="1" Size="TextSize.Xl2">Activity</Heading>
                    <TextBlock Size="TextSize.Sm" Color="Color.Muted">Who did what in this workspace, newest first.</TextBlock>
                </Stack>
                <Button Variant="ButtonVariant.Outline" Accent="Accent.Neutral" StartIcon="Lucide.Download">Export</Button>
            </Row>
            <Card>
                <CardBody Flush>
                    <Table TItem="AuditEvent" ItemsProvider="LoadEvents" PageSize="12" Paging="DataPaging.LoadOnScroll"
                           MaxHeight='Length.Of("max(16rem, 100dvh - 18rem)")' ItemKey="e => e.Id.ToString()" Caption="Activity" EmptyText="No activity matches these filters">
                        <PropertyColumn Property="e => e.At" Title="Time (UTC)" Format="MMM d, HH:mm" />
                        <PropertyColumn Property="e => e.Who" Filterable />
                        <PropertyColumn Property="e => e.What" Title="Action" Filterable />
                    </Table>
                </CardBody>
            </Card>
        </Stack>
    </AppMain>
</AppShell>

@code {
    // The filters go to the API with the cursor; the table drops the cursor when one changes.
    private static async ValueTask<DataResult<AuditEvent>> LoadEvents(DataRequest<AuditEvent> request)
    {
        var (items, next) = await Events.ListAsync(request.Cursor, request.Take ?? 12,
            request.Filter?.Find("who")?.Value as string, request.Filter?.Find("what")?.Value as string);
        return new DataResult<AuditEvent>(items) { Next = next };
    }
}

Static: The same links, swapped in without a full reload.

Connection lost

Rejoining the server…