admin: live query detail becomes a modal, live/history and period pickers become tabs and radios, client delete confirms in a dialog
the streamed-query detail panel is now a rac modal dialog with focus containment and restore. the live/history switch is rac tabs driven by the url, the overview period picker is a rac radio group, and the clients delete flow uses the shared confirm dialog; an authority turn keeps the dialog open and withdraws only the destructive action.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
|
||||
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
import { Tab, TabList, TabPanel, Tabs } from "react-aria-components";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
import ActivityFilters, { NO_FILTERS, type AppliedFilters } from "./ActivityFilters";
|
||||
@@ -53,6 +54,13 @@ const styles = stylex.create({
|
||||
fontWeight: 500,
|
||||
cursor: "pointer",
|
||||
},
|
||||
/** A Tab is a `div` with a roving tabindex, so RAC drives the ring, not `:focus-visible`. */
|
||||
modeFocusVisible: {
|
||||
outlineWidth: 2,
|
||||
outlineStyle: "solid",
|
||||
outlineColor: colors.focus,
|
||||
outlineOffset: 2,
|
||||
},
|
||||
modeIdle: {
|
||||
backgroundColor: { default: "transparent", ":hover": colors.surfaceHover },
|
||||
color: { default: colors.textSecondary, ":hover": colors.text },
|
||||
@@ -76,8 +84,30 @@ const styles = stylex.create({
|
||||
lineHeight: "1.25rem",
|
||||
color: colors.textMuted,
|
||||
},
|
||||
panel: {
|
||||
outlineStyle: "none",
|
||||
},
|
||||
/**
|
||||
* A panel with nothing tabbable in it — an empty or loading history — is given
|
||||
* a tabindex by RAC so the reader can still reach its content, so it has to be
|
||||
* able to show that it holds the focus.
|
||||
*/
|
||||
panelFocusVisible: {
|
||||
outlineWidth: 2,
|
||||
outlineStyle: "solid",
|
||||
outlineColor: colors.focus,
|
||||
outlineOffset: 2,
|
||||
},
|
||||
/** Explicit, so RAC's default `react-aria-Tabs` class does not land instead. */
|
||||
tabsRoot: {
|
||||
display: "block",
|
||||
},
|
||||
});
|
||||
|
||||
function panelClass({ isFocusVisible }: { isFocusVisible: boolean }): string {
|
||||
return stylex.props(styles.panel, isFocusVisible && styles.panelFocusVisible).className ?? "";
|
||||
}
|
||||
|
||||
export default function ActivityPage() {
|
||||
const search = useSearch({ from: "/shell/activity" });
|
||||
const navigate = useNavigate({ from: "/activity" });
|
||||
@@ -97,56 +127,66 @@ export default function ActivityPage() {
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div {...stylex.props(styles.header)}>
|
||||
<h1 {...stylex.props(styles.heading)}>Activity</h1>
|
||||
<div role="group" aria-label="Activity mode" {...stylex.props(styles.switch)}>
|
||||
{MODES.map((option) => {
|
||||
const selected = option.mode === search.mode;
|
||||
return (
|
||||
<button
|
||||
{/*
|
||||
* The selected tab is the URL's `mode` and nothing else. RAC would hold
|
||||
* the selection itself, but a second copy of it would fight the back
|
||||
* button, so the search parameter stays the only state there is.
|
||||
*/}
|
||||
<Tabs
|
||||
selectedKey={search.mode}
|
||||
onSelectionChange={(key) => selectMode(key as ActivityMode)}
|
||||
className={() => stylex.props(styles.tabsRoot).className ?? ""}
|
||||
>
|
||||
<div {...stylex.props(styles.header)}>
|
||||
<h1 {...stylex.props(styles.heading)}>Activity</h1>
|
||||
<TabList
|
||||
aria-label="Activity mode"
|
||||
className={() => stylex.props(styles.switch).className ?? ""}
|
||||
>
|
||||
{MODES.map((option) => (
|
||||
<Tab
|
||||
key={option.mode}
|
||||
type="button"
|
||||
aria-pressed={selected}
|
||||
onClick={() => selectMode(option.mode)}
|
||||
{...stylex.props(
|
||||
styles.modeButton,
|
||||
selected ? styles.modeSelected : styles.modeIdle,
|
||||
shared.focusRing,
|
||||
)}
|
||||
id={option.mode}
|
||||
className={({ isSelected, isFocusVisible }) =>
|
||||
stylex.props(
|
||||
styles.modeButton,
|
||||
isSelected ? styles.modeSelected : styles.modeIdle,
|
||||
isFocusVisible && styles.modeFocusVisible,
|
||||
).className ?? ""
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<Link to="/activity/test" {...stylex.props(styles.simulationLink, shared.focusRing)}>
|
||||
Current policy simulation
|
||||
</Link>
|
||||
</div>
|
||||
<Link to="/activity/test" {...stylex.props(styles.simulationLink, shared.focusRing)}>
|
||||
Current policy simulation
|
||||
</Link>
|
||||
</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.
|
||||
*/}
|
||||
<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)}
|
||||
/>
|
||||
{/*
|
||||
* 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.
|
||||
*/}
|
||||
<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)}
|
||||
/>
|
||||
|
||||
{live ? (
|
||||
<>
|
||||
<TabPanel id="history" className={panelClass}>
|
||||
<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.
|
||||
</p>
|
||||
<LiveActivity origin={search} />
|
||||
</>
|
||||
) : (
|
||||
<HistoryActivity search={search} />
|
||||
)}
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user