admin: activity history filters become a live-apply toolbar
Gates / frontend (push) Failing after 1m4s
Gates / package (push) Skipped
Gates / container (push) Skipped
Gates / test (push) Successful in 2m33s
Gates / test-aarch64 (push) Successful in 7m33s
CI / gates (push) Failing after 10m6s

the five-field form with its apply button is gone. one row holds a search-shaped domain field and a client ip field that debounce into the url with enter flushing at once, result segments and a time menu that commit instantly, and a clear that appears only when a filter is active without reflowing the row. presets freeze both absolute bounds at click time so a bookmark describes the same investigation later, custom ranges apply atomically through set range with inline validation, and echo queues keep in-flight commits from clobbering newer typing or newer picks. live mode renders no toolbar, the availability banner became a footer note, previous rows stay visible during refetch, and every control carries a 44px hit target.
This commit is contained in:
2026-08-29 14:56:40 +02:00
parent c65d92d8f8
commit 039eeeda96
7 changed files with 1107 additions and 233 deletions
+19 -17
View File
@@ -8,6 +8,7 @@
* recipient should be looking at.
*/
import { useCallback } from "react";
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import * as stylex from "@stylexjs/stylex";
import { Tab, TabList, TabPanel, Tabs } from "react-aria-components";
@@ -111,8 +112,6 @@ function panelClass({ isFocusVisible }: { isFocusVisible: boolean }): string {
export default function ActivityPage() {
const search = useSearch({ from: "/shell/activity" });
const navigate = useNavigate({ from: "/activity" });
const live = search.mode === "live";
// The functional form, not a replacement object: the filters are retained
// across a mode switch on purpose, and spelling out a new search here would
// drop every one of them on the way to Live and back.
@@ -121,9 +120,19 @@ export default function ActivityPage() {
void navigate({ search: (prev) => ({ ...prev, mode }) });
}
function apply(filters: AppliedFilters) {
void navigate({ search: { mode: search.mode, ...filters } });
}
// A patch, merged into whatever the URL already says: a segment click must not
// spell out the four filters it is not about. `replace` is the debounced text
// commits, so the back button steps between investigations, not keystrokes.
const apply = useCallback(
(patch: Partial<AppliedFilters>, replace = false) => {
void navigate({ search: (prev) => ({ ...prev, ...patch }), replace });
},
[navigate],
);
const clear = useCallback(() => {
void navigate({ search: (prev) => ({ mode: prev.mode, ...NO_FILTERS }) });
}, [navigate]);
return (
<section>
@@ -165,24 +174,17 @@ export default function ActivityPage() {
</div>
{/*
* Remounted whenever the applied search changes, which is what makes
* the back button work: the draft is derived state, and the browser
* moving the URL under it has to move the form with it.
* The toolbar belongs to History alone. It is not remounted on a search
* change: it resyncs its draft from the URL instead, because a remount
* mid-debounce would take the focus out of the input being typed in.
*/}
<ActivityFilters
key={`${search.domain ?? ""}|${search.client ?? ""}|${String(search.blocked)}|${String(search.since)}|${String(search.until)}`}
applied={search}
isDisabled={live}
onApply={apply}
onClear={() => apply(NO_FILTERS)}
/>
<TabPanel id="history" className={panelClass}>
<ActivityFilters applied={search} onApply={apply} onClear={clear} />
<HistoryActivity search={search} />
</TabPanel>
<TabPanel id="live" className={panelClass}>
<p {...stylex.props(styles.liveNote)}>
The stream carries every query the server answers; these filters apply to history only.
The stream carries every query the server answers; the History filters apply to history only.
</p>
<LiveActivity origin={search} />
</TabPanel>