Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/779-admin-search-ui.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`/admin/search` now uses one filter toolbar with `Jobs` / `Audit` tabs, one Query field, a primary Search action and a secondary export with the active count. Audit rows show human action labels once, render `key=value` details as chips, link job/repository/user ids, and collapse long errors instead of printing one truncated raw line.
19 changes: 12 additions & 7 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,13 +319,18 @@ recorded". `scripts/restore-drill.mjs --status-file` is the writer.
rebuilds that URL keeps the state it is not changing: the 7/30-day window,
both "Clear filters" links and both pagers read the keys from one helper, so
the filter survives a window switch and a page click, and the window survives
a filter and a clear. The admin search page (`/admin/search`) builds its two
job filters from those same `JOB_FILTER_STATUSES` and `JOB_FILTER_KINDS` lists
and the same label helpers, so every kind a job can have is selectable there
and both locales read the console's words rather than the stored code; the
option values stay the codes, so the query, the totals and the CSV export are
unchanged. A second hand-written list in a view is how `scope` and `verify`
went missing from a form once already.
a filter and a clear. The admin search page (`/admin/search`) uses one toolbar
with a `Jobs` / `Audit` switch (`view=jobs|audit`): the active tab owns the
only filter form, so there is one Query field and paging or export cannot drop
the other tab's unrelated keys. The job fields still come from
`JOB_FILTER_STATUSES` and `JOB_FILTER_KINDS` and the same label helpers, so
every kind a job can have is selectable there and both locales read the
console's words rather than the stored code; option values stay the codes, so
the query, totals and CSV export are unchanged. Audit details render
`key=value` pairs as chips, link `job_*`, `repo_*` and `usr_*` ids to their
pages, and collapse long free text behind a details control instead of
truncating one raw line. A second hand-written list in a view is how `scope`
and `verify` went missing from a form once already.
- **Admin CSV exports**: both `/admin/export/jobs.csv` and
`/admin/export/audit.csv` write one `admin.jobs_export` / `admin.audit_export`
row once the body is known — `filters=` echoes the allow-listed codes and ISO
Expand Down
28 changes: 28 additions & 0 deletions public/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,34 @@ textarea { resize: vertical; min-height: 70px; }
.job-filters select { width: 100%; min-height: var(--touch); }
.job-filter-actions { display: flex; align-items: center; gap: 10px; }

/* Admin search: one toolbar, tabs, and a readable audit detail cell. */
.search-head { align-items: flex-start; gap: 18px; flex-wrap: wrap; }
.search-tabs { display: flex; align-items: center; gap: 6px; padding: 3px; border: 1px solid var(--line); border-radius: var(--radius-pill); background: var(--field); }
.search-tab { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border-radius: var(--radius-pill); color: var(--muted); font-size: 13px; text-decoration: none; }
.search-tab.is-active { background: var(--panel); color: var(--text); box-shadow: 0 1px 0 var(--line); }
.search-tab .count { font-variant-numeric: tabular-nums; color: var(--muted-2); font-size: 12px; }
.search-filters { display: grid; gap: 12px; margin: 14px 0 10px; }
.filter-grid { display: grid; grid-template-columns: minmax(220px, 2fr) repeat(3, minmax(140px, 1fr)); gap: 12px; align-items: end; }
.filter-grid label { display: grid; gap: 6px; min-width: 0; font-size: 13px; color: var(--muted); }
.filter-grid input, .filter-grid select { width: 100%; }
.filter-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.filter-actions .btn { flex: 0 0 auto; }
.filter-export { margin-left: auto; }
.audit-detail { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; min-width: 0; }
.audit-detail > span { min-width: 0; overflow-wrap: anywhere; }
.kv { display: inline-flex; align-items: baseline; gap: 4px; padding: 1px 7px; border: 1px solid var(--line); border-radius: var(--radius-pill); background: var(--surface-raised); font-size: 12px; }
.kv b { font-weight: 600; color: var(--muted-2); }
.audit-more { flex-basis: 100%; }
.audit-more summary { cursor: pointer; color: var(--muted-2); font-size: 12px; }
.audit-more pre { margin: 6px 0 0; font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; }
@media (max-width: 900px) { .filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 680px) {
.search-head { flex-direction: column; }
.search-tabs { width: 100%; justify-content: space-between; }
.filter-grid { grid-template-columns: minmax(0, 1fr); }
.filter-export { margin-left: 0; }
}

.pager { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 14px; }
.pager-info { color: var(--muted-2); font-size: 12px; font-variant-numeric: tabular-nums; }

Expand Down
3 changes: 2 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4585,6 +4585,7 @@ export async function buildApp(options: BuildOptions): Promise<BuiltApp> {
const action = (queryText(request, 'action') ?? '').slice(0, 80)
const from = (queryText(request, 'from') ?? '').slice(0, 20)
const to = (queryText(request, 'to') ?? '').slice(0, 20)
const view = queryText(request, 'view') === 'audit' ? 'audit' : 'jobs'
const perPage = 20
const page = (name: string, total: number): number => {
const n = Number(queryText(request, name))
Expand All @@ -4605,7 +4606,7 @@ export async function buildApp(options: BuildOptions): Promise<BuiltApp> {
jobs: store.searchJobs(userIds, jobFilters, perPage, (jobsPage - 1) * perPage),
audit: store.searchAudit(userIds, auditFilters, perPage, (auditPage - 1) * perPage),
usersById,
filters: { q, status, kind, priority, action, from, to },
filters: { q, status, kind, priority, action, from, to, view },
jobsPage,
jobsTotal,
auditPage, auditTotal,
Expand Down
Loading
Loading