Skip to content

Commit 720d74c

Browse files
committed
New: Wire DataViews v16 filters into Logs and Redirects REST endpoints
Refs #68. After upgrading @wordpress/dataviews to v16 every field exposes its type's full operator set, but the REST API only honoured a handful of scalar `is` filters. Bring the supported columns up to parity and turn off filtering on the ones we don't want. Adds a Filter_Mapper that translates DataViews `{field, operator, value}` entries into BerlinDB query args, plus two custom args (`like_query`, `range_query`) handled by a `{plural}_query_clauses` filter on the base model. BerlinDB's native `compare_query` short-circuits without a metadata sidecar, so numeric ranges are emitted by us. Adds KEY ip / KEY hits on logs and KEY hits on redirects (table version bumped to 4.0.2).
1 parent b56896e commit 720d74c

15 files changed

Lines changed: 1358 additions & 145 deletions

File tree

assets/src/hooks/use-logs.js

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,14 @@ import { buildQueryKey } from './persisted-view'
1414

1515
const BASE = '/404-to-301/v1/logs'
1616

17-
/**
18-
* Local-time `YYYY-MM-DD` for `days` ago (0 = today). Backs the
19-
* "First seen" preset-range filter — the endpoint's `date_from`
20-
* expects a plain date string.
21-
*
22-
* @param {number} days Days to subtract from today.
23-
* @return {string} Date in `YYYY-MM-DD`.
24-
*/
25-
const daysAgoISO = (days) => {
26-
const d = new Date()
27-
d.setDate(d.getDate() - days)
28-
const mm = String(d.getMonth() + 1).padStart(2, '0')
29-
const dd = String(d.getDate()).padStart(2, '0')
30-
return `${d.getFullYear()}-${mm}-${dd}`
31-
}
32-
3317
/**
3418
* Translate a DataViews `view` object into REST query parameters.
3519
*
20+
* `view.filters` is forwarded verbatim as a structured `filters[]`
21+
* array. The server-side mapper (`Filter_Mapper::for_logs()`) maps
22+
* each `{ field, operator, value }` entry onto the appropriate
23+
* BerlinDB clause (`IN`, `NOT IN`, `compare_query`, or a LIKE).
24+
*
3625
* @param {Object} view DataViews view state.
3726
* @return {Object} Query args for the logs endpoint.
3827
*/
@@ -51,30 +40,21 @@ const viewToQuery = (view) => {
5140
query.order = view.sort.direction === 'asc' ? 'asc' : 'desc'
5241
}
5342

54-
// Filters such as `status` come through `view.filters` as
55-
// `{ field, operator, value }`. Forward each as a query arg.
5643
if (Array.isArray(view.filters)) {
57-
view.filters.forEach((filter) => {
58-
if (
59-
!filter ||
60-
!filter.field ||
61-
filter.value === undefined ||
62-
filter.value === ''
63-
) {
64-
return
65-
}
66-
67-
// "First seen" is a preset-range filter (its value is a
68-
// number of days, see `dateRanges`). DataViews has no date
69-
// operator, so we translate the preset into the endpoint's
70-
// `date_from` rather than forwarding `created_at` verbatim.
71-
if (filter.field === 'created_at') {
72-
query.date_from = daysAgoISO(Number(filter.value) || 0)
73-
return
74-
}
75-
76-
query[filter.field] = filter.value
77-
})
44+
const filters = view.filters
45+
.filter(
46+
(f) =>
47+
f &&
48+
f.field &&
49+
f.operator &&
50+
f.value !== undefined &&
51+
f.value !== '' &&
52+
!(Array.isArray(f.value) && f.value.length === 0),
53+
)
54+
.map(({ field, operator, value }) => ({ field, operator, value }))
55+
if (filters.length > 0) {
56+
query.filters = filters
57+
}
7858
}
7959

8060
return query

assets/src/hooks/use-redirects.js

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,20 @@ const viewToQuery = (view) => {
3030
}
3131

3232
if (Array.isArray(view.filters)) {
33-
view.filters.forEach((filter) => {
34-
if (
35-
filter &&
36-
filter.field &&
37-
filter.value !== undefined &&
38-
filter.value !== ''
39-
) {
40-
query[filter.field] = filter.value
41-
}
42-
})
33+
const filters = view.filters
34+
.filter(
35+
(f) =>
36+
f &&
37+
f.field &&
38+
f.operator &&
39+
f.value !== undefined &&
40+
f.value !== '' &&
41+
!(Array.isArray(f.value) && f.value.length === 0),
42+
)
43+
.map(({ field, operator, value }) => ({ field, operator, value }))
44+
if (filters.length > 0) {
45+
query.filters = filters
46+
}
4347
}
4448

4549
return query

assets/src/modules/logs/fields.js

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,6 @@ const statusMeta = {
1616
2: { slug: 'fixed', icon: published },
1717
}
1818

19-
// Preset ranges for the "First seen" filter. DataViews has no date
20-
// operator, so the range is offered as a single-select of relative
21-
// windows — each value is a day count that `useLogs` turns into the
22-
// endpoint's `date_from` (0 = today).
23-
const dateRanges = [
24-
{ value: '0', label: __('Today', '404-to-301') },
25-
{ value: '7', label: __('Last 7 days', '404-to-301') },
26-
{ value: '30', label: __('Last 30 days', '404-to-301') },
27-
{ value: '90', label: __('Last 90 days', '404-to-301') },
28-
{ value: '365', label: __('Last 12 months', '404-to-301') },
29-
]
30-
3119
const empty = <span className="d404-empty">{'—'}</span>
3220

3321
/**
@@ -70,6 +58,11 @@ export const fields = [
7058
type: 'text',
7159
enableGlobalSearch: true,
7260
enableSorting: true,
61+
// IPs live in a packed VARBINARY column. LIKE / contains over
62+
// packed bytes can't match user input, so only exact / IN
63+
// operators are exposed — the server packs the value via
64+
// `inet_pton()` before comparing.
65+
filterBy: { operators: ['is', 'isNot', 'isAny', 'isNone'] },
7366
render: ({ item }) => (item.ip ? <Truncate value={item.ip} /> : empty),
7467
},
7568
{
@@ -78,6 +71,10 @@ export const fields = [
7871
type: 'text',
7972
enableGlobalSearch: true,
8073
enableSorting: false,
74+
// User-agent strings are noisy and high-cardinality. The
75+
// global search still hits this column; a dedicated DV filter
76+
// would just clutter the picker.
77+
filterBy: false,
8178
render: ({ item }) => (item.ua ? <Truncate value={item.ua} /> : empty),
8279
},
8380
{
@@ -92,7 +89,9 @@ export const fields = [
9289
label: __('Status', '404-to-301'),
9390
type: 'integer',
9491
elements: statusElements,
95-
filterBy: { operators: ['is', 'isNot'] },
92+
// Status is an enum: only the membership operators apply.
93+
// Excludes the integer defaults (`lessThan`, `between`, …).
94+
filterBy: { operators: ['is', 'isNot', 'isAny', 'isNone'] },
9695
render: ({ item }) => {
9796
const label =
9897
statusElements.find((el) => el.value === item.status)?.label ||
@@ -132,10 +131,11 @@ export const fields = [
132131
label: __('First seen', '404-to-301'),
133132
type: 'datetime',
134133
enableSorting: true,
135-
// Preset relative-window filter (Today / Last 7 days / …).
136-
// `useLogs` maps the chosen day count to `date_from`.
137-
elements: dateRanges,
138-
filterBy: { operators: ['is'] },
134+
// Date columns are sortable but not filterable: DV16's
135+
// datetime operator set (`before`, `after`, `inThePast`, …)
136+
// would need date_query plumbing on the API that we haven't
137+
// committed to yet.
138+
filterBy: false,
139139
render: ({ item }) =>
140140
item.created_at ? (
141141
<time dateTime={item.created_at}>
@@ -150,6 +150,7 @@ export const fields = [
150150
label: __('Last hit', '404-to-301'),
151151
type: 'datetime',
152152
enableSorting: true,
153+
filterBy: false,
153154
render: ({ item }) =>
154155
item.updated_at ? (
155156
<time dateTime={item.updated_at}>

assets/src/modules/redirects/fields.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ export const fields = [
8585
type: 'integer',
8686
elements: redirectTypes,
8787
enableSorting: true,
88-
filterBy: { operators: ['is', 'isNot'] },
88+
// Status codes are an enum — only membership operators apply.
89+
filterBy: { operators: ['is', 'isNot', 'isAny', 'isNone'] },
8990
// Show the bare status code in the column so it fits the narrow
9091
// width; the full label (e.g. "301 — Moved Permanently (SEO)")
9192
// stays available on hover.
@@ -105,26 +106,28 @@ export const fields = [
105106
label: __('Match', '404-to-301'),
106107
type: 'text',
107108
elements: matchTypes,
108-
filterBy: { operators: ['is', 'isNot'] },
109+
filterBy: { operators: ['is', 'isNot', 'isAny', 'isNone'] },
109110
render: ({ item }) => findLabel(matchTypes, item.match_type),
110111
},
111112
{
112-
// Filter-only by default (not in `defaultView.fields`), so it
113-
// adds a "Destination type" filter without forcing the column
114-
// on — the Destination column already shows link vs. page.
115113
id: 'target_type',
116114
label: __('Destination type', '404-to-301'),
117115
type: 'text',
118116
elements: targetTypes,
119-
filterBy: { operators: ['is', 'isNot'] },
117+
// The Destination column already shows link vs. page; an
118+
// explicit destination-type filter is more clutter than
119+
// signal here, so leave it column-only.
120+
filterBy: false,
120121
render: ({ item }) => findLabel(targetTypes, item.target_type),
121122
},
122123
{
123124
id: 'is_active',
124125
label: __('Status', '404-to-301'),
125126
type: 'boolean',
126127
elements: activeStates,
127-
filterBy: { operators: ['is'] },
128+
// `isNot` is collapsed back to `is` (with the flipped bool) on
129+
// the server side — see {@see Filter_Mapper::translate()}.
130+
filterBy: { operators: ['is', 'isNot'] },
128131
render: ({ item }) => {
129132
const label = item.is_active
130133
? __('Active', '404-to-301')
@@ -158,6 +161,9 @@ export const fields = [
158161
label: __('Last hit', '404-to-301'),
159162
type: 'datetime',
160163
enableSorting: true,
164+
// Date columns are sortable but not filterable — see the
165+
// matching note in logs/fields.js.
166+
filterBy: false,
161167
render: ({ item }) =>
162168
item.last_hit_at ? (
163169
<time dateTime={item.last_hit_at}>
@@ -172,6 +178,7 @@ export const fields = [
172178
label: __('Created', '404-to-301'),
173179
type: 'datetime',
174180
enableSorting: true,
181+
filterBy: false,
175182
render: ({ item }) =>
176183
item.created_at ? (
177184
<time dateTime={item.created_at}>
@@ -190,6 +197,9 @@ export const fields = [
190197
label: __('Last edited by', '404-to-301'),
191198
type: 'text',
192199
enableSorting: true,
200+
// We don't ship a user-picker for filtering by author; keep
201+
// the column sortable but off the filter list.
202+
filterBy: false,
193203
render: ({ item }) =>
194204
item.modified_by_name ? (
195205
<span className="d404-modified-by">

0 commit comments

Comments
 (0)