-
Notifications
You must be signed in to change notification settings - Fork 0
feat(web): cascading record-type filter on the Audit page #521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,14 @@ import { useTranslation } from "react-i18next"; | |
| import { useSearchParams } from "react-router"; | ||
| import { listAuditEvents } from "../api/cluckwork"; | ||
| import { usePagedList } from "../components/usePagedList"; | ||
| import { AUDIT_ACTION_VALUES, auditActionLabel, entityTypeLabel } from "../i18n/enums"; | ||
| import { | ||
| AUDIT_ACTION_ENTITY_TYPE, | ||
| AUDIT_ACTION_VALUES, | ||
| auditActionLabel, | ||
| ENTITY_TYPE_VALUES, | ||
| entityTypeLabel, | ||
| type EntityTypeValue, | ||
| } from "../i18n/enums"; | ||
|
|
||
| const PAGE = 100; | ||
|
|
||
|
|
@@ -56,7 +63,7 @@ export function AuditPage() { | |
| const { t: tc } = useTranslation("common"); | ||
|
|
||
| const [searchParams, setSearchParams] = useSearchParams(); | ||
| const actionFilter = searchParams.get("action") ?? ""; | ||
| const rawActionFilter = searchParams.get("action") ?? ""; | ||
| const rawEntityId = searchParams.get("entityId"); | ||
| // Lowercased (codex review of #516): the API returns EntityId as a .NET | ||
| // Guid, which System.Text.Json serializes lowercase regardless of the | ||
|
|
@@ -75,6 +82,43 @@ export function AuditPage() { | |
| setSearchParams(next); | ||
| }, [searchParams, setSearchParams]); | ||
|
|
||
| // Entity-type filter narrows the action dropdown's OPTION LIST only — it is | ||
| // never sent to the server (the /api/v1/audit query still filters on | ||
| // `action` alone, matching what the backend supports). Changing it drops | ||
| // any selected `action`: AUDIT_ACTION_ENTITY_TYPE is a many-to-one map, so | ||
| // a previously chosen action can fall outside the new type's option list, | ||
| // and leaving it selected-but-hidden would silently keep querying against | ||
| // a type the visible dropdown no longer shows. | ||
| const rawEntityTypeFilter = searchParams.get("entityType"); | ||
| const entityTypeFilter: EntityTypeValue | "" = | ||
| rawEntityTypeFilter && (ENTITY_TYPE_VALUES as readonly string[]).includes(rawEntityTypeFilter) | ||
| ? (rawEntityTypeFilter as EntityTypeValue) | ||
| : ""; | ||
|
|
||
| const updateEntityTypeFilter = useCallback((type: string) => { | ||
| const next = new URLSearchParams(searchParams); | ||
| if (type) next.set("entityType", type); | ||
| else next.delete("entityType"); | ||
| next.delete("action"); | ||
| setSearchParams(next); | ||
| }, [searchParams, setSearchParams]); | ||
|
|
||
| const availableActions = entityTypeFilter | ||
| ? AUDIT_ACTION_VALUES.filter((a) => AUDIT_ACTION_ENTITY_TYPE[a] === entityTypeFilter) | ||
| : AUDIT_ACTION_VALUES; | ||
|
|
||
| // codex review of #521 — a hand-edited or shared URL can carry a valid but | ||
| // INCOMPATIBLE pair, e.g. ?entityType=Flock&action=User.Create: the raw | ||
| // action isn't in `availableActions` for that type, so the <select> falls | ||
| // back to showing no match while `fetchPage` would silently keep querying | ||
| // the hidden action. Ignoring an out-of-scope action here, not just when | ||
| // the user changes the type dropdown, keeps the visible filter and the | ||
| // query in sync for BOTH entry paths (a click, and a direct URL load). | ||
| const actionFilter = | ||
| rawActionFilter && (availableActions as readonly string[]).includes(rawActionFilter) | ||
| ? rawActionFilter | ||
| : ""; | ||
|
|
||
| // #469 — the ticket/dedupe/busy-ownership discipline this screen grew for | ||
| // itself (codex review of #94) now lives in usePagedList, shared with every | ||
| // other paged screen. The filter is expressed as the fetcher's identity, so | ||
|
|
@@ -169,10 +213,18 @@ export function AuditPage() { | |
| <p className="muted">{t("intro")}</p> | ||
|
|
||
| <div className="filters"> | ||
| <label>{t("entityTypeFilterLabel")} | ||
| <select value={entityTypeFilter} onChange={(e) => updateEntityTypeFilter(e.target.value)}> | ||
|
Comment on lines
215
to
+217
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This adds a user-visible control with non-obvious semantics—the record type narrows the Action choices but does not itself filter audit rows—without updating the SPA Help page or its in-app glossary. Add guidance to the Audit help section so users can understand the two-step filtering behavior, as required for every user-visible behavior change. AGENTS.md reference: AGENTS.md:L251-L251 Useful? React with 👍 / 👎.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| <option value="">{t("allEntityTypesOption")}</option> | ||
| {ENTITY_TYPE_VALUES.map((et) => ( | ||
| <option key={et} value={et}>{entityTypeLabel(et)}</option> | ||
| ))} | ||
| </select> | ||
| </label> | ||
| <label>{t("actionFilterLabel")} | ||
| <select value={actionFilter} onChange={(e) => updateActionFilter(e.target.value)}> | ||
| <option value="">{t("allActionsOption")}</option> | ||
| {AUDIT_ACTION_VALUES.map((a) => ( | ||
| {availableActions.map((a) => ( | ||
| <option key={a} value={a}>{auditActionLabel(a)}</option> | ||
| ))} | ||
| </select> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a shared or hand-edited URL contains a valid but incompatible pair such as
?entityType=Flock&action=User.Create, this list omits the selected action, so the Action control visually falls back to “All actions” whilefetchPagecontinues queryingUser.Create. ValidateactionFilteragainstavailableActionsand clear or ignore it when the pair is incompatible so a hidden filter cannot produce misleading results.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 983af91:
actionFilteris now validated againstavailableActionsfor the currententityTypeFilterand ignored (not just reset on a dropdown click) when the URL's action isn't in scope for its type — closes it for both a manual click and a direct/shared URL load. Mutation-verified: reverting the validation makes the new test fail on its ownaction: undefinedassertion.