Warning, /swf-monitor/docs/INCLUSIVE_FILTER.md is written in an unsupported language. File is not indexed.
0001 # Inclusive filter
0002
0003 A facet filter whose selections widen the shown set instead of narrowing
0004 it, applied in the browser. `monitor_app/inclusive_filter.py` and the
0005 include `monitor_app/_inclusive_filter.html`; first used on the campaign
0006 plan page (swf-epicprod `pcs/views.py`, `pcs_campaign_plan`).
0007
0008 ## Behavior
0009
0010 - Every facet lists every value with its count over all rows. Nothing
0011 disappears when a selection is made; a selected value with no rows
0012 stays listed at zero.
0013 - Clicking a value selects it, shown bold and underlined. The Match
0014 control at the top decides what the selections show. Match any, the
0015 default: the union of every selected value, across facets as well as
0016 within one, so Process DIS plus Beam 10x100 shows the rows that are
0017 DIS or at 10x100. Match all: only the rows carrying every selected
0018 value, so the same two show the DIS rows at 10x100. The control
0019 states both in one line, in the page's own noun (the plan says
0020 "configurations"; `InclusiveFilter(..., noun=...)`, default "rows").
0021 - Clicking a selected value deselects it. A facet's All clears that
0022 facet. Clear all clears everything. With nothing selected every row
0023 is shown.
0024 - The statement under the facet rows names the selections in click
0025 order: "Showing rows matching any of: Process: DIS Beam: 10x100"
0026 ("all of" under Match all).
0027 - A click costs no request. The server renders every row once with its
0028 facet values and the initial hidden state; the include's script
0029 applies each click to the page: the bold marks, the rows, the shown
0030 count, the statement, the URL. On the campaign plan, 1070 rows, the
0031 table changes in about 50 ms.
0032
0033 ## URL contract
0034
0035 One parameter carries the state: `f`, the selections as `facet:value`
0036 pairs joined by `|`, in click order, rewritten with `replaceState` on
0037 every click, so the address bar always holds the current state and a
0038 bookmark opens it. Back leaves the page.
0039
0040 ?f=process:DIS|beam:10x100
0041
0042 Match all adds `match=all`; Match any, the default, is the parameter's
0043 absence.
0044
0045 A link may frame the page with `n`, the same syntax: pairs every row
0046 must carry, an intersection, which is what a count on another page
0047 means (the completion panel's "priority 1, below target" cell opens
0048 exactly the rows it counts). Clicks widen within the frame; clear all
0049 drops it. The statement reads "Showing rows within Priority: 1 Status:
0050 below target matching any of: ...".
0051
0052 A page that previously carried one parameter per facet lists them in
0053 `legacy`. Those parameters meant an intersection, so they are read into
0054 the frame and dropped from the URLs the filter writes; old links open
0055 exactly what they opened before. Consumers that carry a page's filter
0056 state forward (the campaign plan's Time history embed and the Snapper
0057 Campaign focus view) carry `n`, `f` and `match`. The search, `q`,
0058 narrows the table in the browser only: no consumer carries it and the
0059 server-side apply does not read it. A value containing `|`
0060 cannot be encoded; no filtered value on the plan carries one.
0061
0062 ## Usage
0063
0064 from monitor_app.inclusive_filter import Facet, InclusiveFilter
0065
0066 facets = [
0067 Facet('process', 'Process', lambda r: r['process']),
0068 Facet('requestor', 'Requestor',
0069 lambda r: r['requestors'] or ['Unassigned']),
0070 Facet('status', 'Status', lambda r: slug_of.get(r['status']),
0071 display=lambda v: label_of.get(v, v), order=[...slugs...]),
0072 ]
0073 flt = InclusiveFilter(request.GET, legacy={'process': 'process'})
0074 shown = flt.annotate(rows_all, facets) # row['if_attr'], row['if_hidden']
0075 context['inclusive_filter'] = flt.context(rows_all, facets, request)
0076
0077 In the template, the include above the table and two things on every
0078 row:
0079
0080 {% include 'monitor_app/_inclusive_filter.html' %}
0081 ...
0082 <tr data-if="{{ r.if_attr }}"{% if r.if_hidden %} class="swf-if-hidden"{% endif %}>
0083
0084 An element with class `swf-if-shown` receives the shown count. Page
0085 scripts that must follow the selection listen for the `swf-if-change`
0086 event on `document`, whose `detail` carries `selections`, `query` (the
0087 `f=...` fragment, URL-encoded) and `shown`, and read the visible rows as
0088 `tr[data-if]:not(.swf-if-hidden)`. A page with its own row selection
0089 (tick boxes, select all) restricts it to visible rows the same way.
0090
0091 Rows live in the house sortable tables (DataTables through
0092 `swf-sortable`), which move row nodes on a sort and never recreate them,
0093 so the hidden class survives sorting; the include restripes the visible
0094 rows after each change and after each sort.
0095
0096 `Facet(key, label, values, display=None, order=None)`: `values(row)`
0097 returns the value or values a row carries (several for a
0098 multi-membership axis; None or empty for none); `display` maps a stored
0099 value to its shown form; `order` is a list of values or a sort key,
0100 else values sort. `InclusiveFilter.echo` is the query fragment that
0101 reproduces the slice, for `urlencode`; `matches`, `apply`,
0102 `active_filters`, and the link builders `toggle_url`, `clear_facet_url`
0103 and `clear_url` serve the server side and the no-script fallback: the
0104 anchors keep server URLs, so a middle click opens the right page.
0105
0106 ## Pages
0107
0108 | Page | Since |
0109 |---|---|
0110 | Campaign plan (`/pcs/plan/`), both the plan and the assembly view; its delivery map follows the selection through the Snapper embed's `setHidden` hook (snapper-ai docs/INTEGRATION.md ยง 4) | 2026-09-10 |
0111
0112 Other pages switch as decided, one at a time, by replacing their facet
0113 construction with the calls above.
0114
0115 ## The narrowing filter
0116
0117 The other house filter, for the operations views: a selection narrows.
0118 `monitor_app/narrowing_filter.py` and the include
0119 `monitor_app/_narrowing_filter.html`; first used on the segfault catalog
0120 (`monitor_app/viewdir/pandamon.py`, `panda_segfaults`).
0121
0122 The two filters answer different questions. The inclusive filter serves
0123 a view where the reader assembles the set to look at (the campaign
0124 plan): selections widen, every value keeps its full count. The
0125 narrowing filter serves a view where the reader asks what a slice looks
0126 like (the operations views): a selection narrows the rows to the
0127 intersection, and every bar re-counts within the selection. A facet's
0128 own bar counts within the other facets' selections, so its alternatives
0129 stay listed with the count each would give; the other bars count
0130 within the whole selection. Picking a queue on the catalog leaves the
0131 class bar showing the classes of that queue's crashes.
0132
0133 ### URL contract
0134
0135 One parameter per facet, its key the facet key, one value
0136 (`?class=sparse&queue=NERSC_Perlmutter_epic`); `q` a free-text search
0137 over every column. A click on the selected value clears it, as in the
0138 inclusive filter; a click on another value replaces the selection. Every bar anchor is a server URL carrying the whole
0139 selection, so the filter works without script and a middle click opens
0140 the right page. Clear all is the page's path.
0141
0142 ### Usage
0143
0144 from monitor_app.narrowing_filter import Facet, NarrowingFilter
0145
0146 facets = [
0147 Facet('class', 'Class', lambda r: r['class'],
0148 display=lambda v: labels.get(v, v), order=[...values...]),
0149 Facet('queue', 'Queue', lambda r: r['queues'] or None),
0150 ]
0151 flt = NarrowingFilter(request.GET, facets)
0152 shown = flt.annotate(rows_all, facets) # row['nf_attr'], row['nf_hidden']
0153 context['narrowing_filter'] = flt.context(rows_all, facets, request)
0154
0155 In the template, `{% include 'monitor_app/_narrowing_filter.html' %}`
0156 above the table renders the bars in the house filter-bar markup, the
0157 search box, and the active-filters line; every row carries
0158 `data-nf="{{ r.nf_attr }}"` and the class `swf-nf-hidden` when
0159 `r.nf_hidden` (`flt.annotate(rows_all, facets)` sets both and returns
0160 the number initially shown), and an element with class `swf-nf-shown`
0161 receives the shown count. As with the inclusive filter, a click costs
0162 no request: the server renders every row once with the request's
0163 selection applied (the no-script state, and what a link opens), and
0164 the include's script hides and shows rows, recounts every bar within
0165 the selection, and rewrites the URL with `replaceState`; the `swf-nf-change`
0166 event on `document` carries the selection, the search and the shown
0167 count to page scripts. `Facet` is the same class both filters use.
0168
0169 ### Pages
0170
0171 | Page | Since |
0172 |---|---|
0173 | Segfault catalog (`/panda/segfaults/`) | 2026-09-11 |
0174
0175 The ePIC queues page and the physics-configuration page carry their own
0176 inline bars, counted over the full set; they switch when named.