feat(admin): activity resource (read-only debug feed)
Last resource lands. /admin/activity tails the activity table — votes, RSVPs, office-hour bookings, roadmap ships, pulse opens — with last-7-days / last-30-days filters. Pure read view: no form, no summary, no ops. src/admin/components/ResourceListView.astro: rows fall back to <div> when the resource has no panel pathway (form: null AND no summary). Activity rows aren't clickable now — previously they'd dirty the URL with a ?edit= that resolved to nothing. The registry is complete: 7 resources across 3 groups, matching the sidebar layout described in the spec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e9a986d484
commit
18d371b368
3 changed files with 121 additions and 8 deletions
|
|
@ -95,6 +95,10 @@ function withParams(overrides: Record<string, string | number | null>): string {
|
|||
const showNewButton = resource.form !== null && resource.ops.create !== undefined;
|
||||
const hasItems = allItems.length > 0;
|
||||
const hasMatches = pageItems.length > 0;
|
||||
// A row is only clickable when the panel has something to render — either an
|
||||
// editable form or a review summary. Pure debug resources (activity) skip the
|
||||
// anchor wrapper so clicks don't dirty the URL with a ?edit= that goes nowhere.
|
||||
const rowsClickable = resource.form !== null || resource.summary !== undefined;
|
||||
---
|
||||
|
||||
<section class="bs-list">
|
||||
|
|
@ -162,19 +166,21 @@ const hasMatches = pageItems.length > 0;
|
|||
</div>
|
||||
{pageItems.map((item) => {
|
||||
const id = Number(item.id);
|
||||
const Tag = rowsClickable ? 'a' : 'div';
|
||||
const linkProps = rowsClickable
|
||||
? {
|
||||
href: withParams({ edit: id, new: null }),
|
||||
'aria-label': `Open ${resource.singularLabel.toLowerCase()} ${id}`,
|
||||
}
|
||||
: {};
|
||||
return (
|
||||
<a
|
||||
href={withParams({ edit: id, new: null })}
|
||||
class="bs-grid-row"
|
||||
role="row"
|
||||
aria-label={`Edit ${resource.singularLabel.toLowerCase()} ${id}`}
|
||||
>
|
||||
<Tag class="bs-grid-row" role="row" {...linkProps}>
|
||||
{columns.map((col) => (
|
||||
<div class="bs-grid-td" role="cell">
|
||||
<ListCell column={col} item={item} />
|
||||
</div>
|
||||
))}
|
||||
</a>
|
||||
</Tag>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
|||
102
src/admin/resources/activity.ts
Normal file
102
src/admin/resources/activity.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/* ---------------------------------------------------------------------------
|
||||
* Activity resource — read-only debug feed.
|
||||
*
|
||||
* Activity rows are emitted by side effects elsewhere in the app (voting,
|
||||
* RSVPs, roadmap-ship transitions, pulse opens). The admin view is a tail
|
||||
* of the table for monitoring; no create, no edit, no delete.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
import {
|
||||
getAllActivityForAdmin,
|
||||
type ActivityKind,
|
||||
type ActivityRow,
|
||||
} from '../../lib/db';
|
||||
import type { Resource } from '../resource-types';
|
||||
|
||||
const KIND_LABEL: Record<ActivityKind, string> = {
|
||||
voted: 'Voted',
|
||||
rsvped: 'RSVPed',
|
||||
booked_office_hours: 'Booked office hours',
|
||||
roadmap_shipped: 'Roadmap shipped',
|
||||
pulse_opened: 'Pulse opened',
|
||||
};
|
||||
|
||||
const KIND_PILL_CLASS: Record<ActivityKind, string> = {
|
||||
voted: 'pill-update',
|
||||
rsvped: 'pill-published',
|
||||
booked_office_hours: 'pill-bts',
|
||||
roadmap_shipped: 'pill-shipping',
|
||||
pulse_opened: 'pill-pending',
|
||||
};
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
export const activityResource: Resource<ActivityRow> = {
|
||||
key: 'activity',
|
||||
label: 'Activity',
|
||||
pluralLabel: 'Activity',
|
||||
singularLabel: 'Event',
|
||||
groupKey: 'system',
|
||||
description: 'Recent member actions: votes, RSVPs, office-hour bookings, pulse opens, roadmap ships.',
|
||||
|
||||
list: {
|
||||
queryFn: () => getAllActivityForAdmin(200),
|
||||
columns: [
|
||||
{
|
||||
key: 'actor_name',
|
||||
label: 'Actor',
|
||||
primary: true,
|
||||
width: '1.5fr',
|
||||
render: (item) => ({
|
||||
title: item.actor_name,
|
||||
subtitle: item.actor_role,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'kind',
|
||||
label: 'Kind',
|
||||
kind: 'pill',
|
||||
width: '160px',
|
||||
pillVariants: Object.fromEntries(
|
||||
(Object.keys(KIND_LABEL) as ActivityKind[]).map((k) => [
|
||||
k,
|
||||
{ label: KIND_LABEL[k], class: KIND_PILL_CLASS[k] },
|
||||
]),
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'subject',
|
||||
label: 'Subject',
|
||||
width: '1fr',
|
||||
render: (item) => ({
|
||||
title: `${item.subject_type} #${item.subject_id}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'created_at',
|
||||
label: 'When',
|
||||
kind: 'relative-date',
|
||||
width: '120px',
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
{ key: 'all', label: 'All', predicate: () => true, isDefault: true },
|
||||
{
|
||||
key: 'last_7_days',
|
||||
label: 'Last 7 days',
|
||||
predicate: (i) => Date.now() - new Date(i.created_at).getTime() < 7 * DAY_MS,
|
||||
},
|
||||
{
|
||||
key: 'last_30_days',
|
||||
label: 'Last 30 days',
|
||||
predicate: (i) => Date.now() - new Date(i.created_at).getTime() < 30 * DAY_MS,
|
||||
},
|
||||
],
|
||||
defaultSort: { key: 'created_at', direction: 'desc' },
|
||||
pageSize: 100,
|
||||
},
|
||||
|
||||
// Pure read view — no form, no summary, no ops, no actions.
|
||||
form: null,
|
||||
ops: {},
|
||||
};
|
||||
|
|
@ -12,6 +12,7 @@ import { eventsResource } from './events';
|
|||
import { usersResource } from './users';
|
||||
import { invitationsResource } from './invitations';
|
||||
import { joinRequestsResource } from './join-requests';
|
||||
import { activityResource } from './activity';
|
||||
|
||||
export const groups: ResourceGroup[] = [
|
||||
{
|
||||
|
|
@ -24,5 +25,9 @@ export const groups: ResourceGroup[] = [
|
|||
label: 'The council',
|
||||
resources: [usersResource, invitationsResource, joinRequestsResource],
|
||||
},
|
||||
{ key: 'system', label: 'System', resources: [] },
|
||||
{
|
||||
key: 'system',
|
||||
label: 'System',
|
||||
resources: [activityResource],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue