Repository navigation
Column filters list a column's values to tick, like Excel, and stay set across refreshes and restarts - #5577
Merged
Merged
Conversation
The desktop and web tests both drive these steps.
…lection in Common (#5565)
…are kept across restarts (#5565)
…rs stay set across restarts (#5565)
…er' and 'origin/dev' into w7-5565w
…' into feature/5565-column-value-filter
…ly rebuilds only on a tick change, store limits on read, retry and warn once, text matches on no-list columns stay in the session, all-servers scope for Alert and Job History, Viewer file under local app data
… and case folding in grid-value-filter.js, shared fixture cases and name pairs, Node required on CI
…cell values, folded search, debounced writes, session-only text on no-list columns, kept value line, FinOps server in the filter key
…ies at once, reason columns get no list, filter store backs off and drops duplicates A column built from other fields (the Alerts Status, Triage and Mute columns) lists the text its cells show again. The desktop Clear the value filter button applies at once. Reason columns get no list on both halves. A failing save backs off from 1.5 s up to 60 s, with the file I/O outside the lock. A file holding the same server and grid twice keeps the last copy.
erikdarlingdata
marked this pull request as ready for review
October 8, 2026 19:01
…ion (the definition suffix covers it), so its line-break pin holds (#5565)
This was referenced Oct 8, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Oct 9, 2026
…kDB memory limit Release checklist step 2b. README.md already covered everything up to #5491. This adds the three user-facing 3.10.0 changes merged after it: the one time range picker (#5570), the Excel-style text column filters (#5577) and Lite's DuckDB memory limit setting (#5553). Collector count, tab lists, edition table, platform notes, permissions and the deprecation callout needed no change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A text column's filter now lists the values in that column, like the filter in Excel. You tick the values you want to see, untick the ones you do not, search the list, and use Select All. Ticking several logins gives the IN() list the reporter asked for, and the text match stays beside the list. The filter stays set when a grid refreshes, when you switch tabs, and when you close and reopen the app. The web pages part is below; the desktop part is added next to it.
SECTION: Added
ENTRY: - Column filters list a text column's values to tick, like Excel, and stay set across refreshes and restarts ([#5577]) - A text column's filter lists its values, with a search box, Select All and "(Blanks)", in Lite, the Darling viewer and the web dashboard. Unticking values hides only them, so a new login after a refresh still shows, and ticking values shows only those. The filter stays set across refreshes, tab switches and restarts, per server, grid and column. Lite and the viewer keep it in a file next to their settings, and the web dashboard keeps it in the browser. Number and time columns keep the text match only. Query text, plan, XML, message, detail, description, reason, error and script columns also keep the text match only, and that match lasts for the session.
Web pages
What changes in the web grids (
Darling/PerformanceMonitor.Darling.Service/wwwroot/):js/grid-value-filter.js(new): the value part of a filter as{ mode, set, blank }(modenone,hideorshowOnly), the distinct-value list (ordinal, ignoring case, first spelling stands, sorted), the match, the mode choice when ticks change (all ticked is none; fewer unticked than ticked hides the unticked; otherwise shows only the ticked), the chip words ("hides 3 values", "shows 2 values"), and the kept copy:serializeFiltersandparseFilters.js/panels.js(gridFilter): the popup gets a search box, a Select All checkbox, "(Blanks)" first when any cell is null, empty or whitespace, one checkbox per value, a note "Showing 1,000 of N values. Search to find the rest." above 1,000 values, and "No values are ticked, so no rows show." A row has to pass the text match AND the value part, and columns still AND together. The column's button shows active when either part is. Every value is a text node (el(..., { text })), the checkboxes carryaria-labels, Escape closes the box, and the 60 s repaint and an in-place reconcile (Alert History) both re-list an open box. Clear (in the box), the chip's x and Clear all filters clear both parts.valueList: false(about 20 descriptors injs/pages/server-tabs.js,pages/finops/high-impact.js,index-analysis.js,optimization.jsandjs/read-fields.js); plan columns already hadfilter: false.localStoragekeypm.gridFilters.v1: the active filters only, per route (which carries the server) + grid + columns, at most 200 grids (the least recently used go first). Every read and write is in try/catch; a missing, unreadable or refused store starts empty, never stops a grid from drawing, and warns once. A stored shape is checked field by field on the way in.css/app.css:.grid-filter-values,.gfv-*, using the theme's variables only.Decisions worth a second look:
Desktop (Lite and the Darling Viewer)
Lite and the Viewer share the filter code (
PerformanceMonitor.Common,PerformanceMonitor.Ui), so every filtered grid in both gets the list, and both apps are wired the same way.PerformanceMonitor.Common/Models/ColumnFilterState.cs: the state gains a value part,ValueMode(None,ShowOnly,Hide),Values(ordinal, ignoring case) andValueBlank(a flag of its own, so a real value "(Blanks)" stays a normal value).IsActiveis the text match or the value part; the chip text reads "hides 3 values", "shows 2 values" plus the text match.PerformanceMonitor.Common/Services/ColumnFilterMatcher.cs: a row passes when it passes the text match AND the value part (null, empty and whitespace cells are the blank flag's).PerformanceMonitor.Common/Services/ColumnValueList.cs(new):ColumnValueCatalog(distinct, ignore case, sorted, blank flag, 1,000 cap and its note, search, the 256-character limit) andColumnValueSelection(the mode choice, and a stored value the rows no longer hold is kept, so an unchanged Apply does not drop it).ColumnValueListColumns.cs(new) leaves query, statement, SQL, plan, XML, prose and display-string columns (names ending Text, Formatted, Display, Message, Definition, and so on) to the text match.PerformanceMonitor.Ui/ColumnFilterPopup.xaml(.cs),ColumnValueListModel.cs(new): a search box, Select All (three-state), a virtualised checklist, the cap and no-ticks notes, "(Blanks)" first, the text match kept below it, and "Clear all filters". Tab order follows the layout, the list is one tab stop with arrow keys inside, Space ticks, Esc closes from anywhere, Enter applies. It uses the same theme brushes the old popup did, so Lite's and the Viewer's themes both apply.DataGridFilterManager.cs: values come from the rows the grid holds before any filter; no list for a non-string property, a column that sorts by another member (a time shown as text), a value over 256 characters, or an excluded name.UpdateDataalready re-applied filters; it now also loads the stored ones on the first refresh under a server scope.ClearFilters(the Viewer grid column filters survive Refresh and server switches — a stale filter silently empties a grid while its counts stay full #2306 server-switch path) leaves the stored copy alone, and the next refresh loads the new server's; the newClearAllFilters(the user's button) also removes the stored copy.ColumnFilterStore.cs,ColumnFilterScope.cs(new): one JSON file per app (column-filters.jsonbeside Lite'ssettings.json, and beside the Viewer'sviewer-preferences.json), written 1.5 seconds after the last change, by temp file then replace, and flushed on exit. Key: server + grid + column. Active filters only. At most 200 grids (least recently used go first); a column naming more than 1,000 values is not stored at all (cutting a set would hide the wrong rows), though it still filters in the grid. A file that cannot be read is logged once and ignored; a store that throws never blocks a grid. The file holds the ticked or typed values and the server, grid and column names, nothing else.ServerTab: the server's id;ViewerServerTab: the server name) and the grid'sName, so the 105 filter managers needed no one-by-one edit. The FinOps tab in each app has its own server picker, so its scope follows the picker (set before the same-server early return).ColumnFilterStore.Installis called once in each app's startup.Checked in both apps: the grid names (a census reads every
DataGridFilterManagerunder a scope and fails on a grid with nox:Namein its XAML or two grids with one name under one scope; none today), both startup installs, both scope sets, the FinOps pickers, and that neither app carries its own filter class.Test plan
GridValueFilterBehaviourTests(new): Total 19, Failed 0. It drives all 10 shared cases inFixtures/column-value-filter-cases.jsonthrough the real boxes in Node (tick, untick, tick only, Select All, search, text match, refresh), plus Select All under a search, the 1,000 cap note, a value like<b>x</b>built as text, which columns get a list, labels and Escape, chips, a reconcile in place, the kept copy (round trip, refresh, tab switch, restart, Clear all), a bad or refused store, and the bounds (200 grids, 1,000 values).panels.jsreverted to dev, all 19 fail; restored, all 19 pass.GridFilterBehaviourTests(the existing text filter): Total 15, Failed 0.WebModuleLoadTestsTotal 7, Failed 0 (it caught a comment naming an export; reworded). The*Web*classes ran Total 1,619 with that one failure (Skipped 62); after the fixWebModuleLoadTestswas re-run alone, and the new and filter classes again after merging dev.*Census*Total 477 Failed 0;*Ratchet*Total 15 Failed 0;*Hygiene*Total 90 Failed 0;*Scrub*Total 120 Failed 0;*Guard*Total 338 Failed 0 (Darling.Tests, one class at a time).Desktop
ColumnValueFilterCasesTests(new): Total 11, Failed 0. It drives all 10 cases ofFixtures/column-value-filter-cases.jsonthrough the shared code (the web part reads the same file).ColumnValueListTests(new): Total 35, Failed 0 (list builder, cap and search notes, mode choice, the match, chip text, excluded columns, Select All under a search past the cap).ColumnFilterPersistenceTests(new): Total 25, Failed 0 (store round trip, per-server separation, least-recently-used bound, 1,000-value bound, bad file ignored and logged once, a cleared filter removed, debounce and atomic replace; the grid manager acrossUpdateData, a restart, a server switch, "Clear all filters", and a store that fails).ColumnFilterPopupTests(new): Total 8, Failed 0 (the real popup controls).ColumnFilterWiringCensusTests(new): Total 6, Failed 0.ColumnFilterMatcher, 12 of the fixture and list tests fail; with the stored-filter load switched off, 4 of the persistence tests fail. Both restored.*ColumnFilter*,*ColumnValue*,*Census*,*Ratchet*,*Hygiene*,*Scrub*,*Guard*on the merged head: Total 1,050, Failed 0, Skipped 60. Lite.Tests the same five wildcards plus the grid and server-tab pins: Total 266, Failed 0. Lite, the Viewer and both test projects build with 0 warnings.Round 2
Head d40edec. The desktop half is done and tested; the web page half is partly done (see Not done).
Done on the desktop (Lite and the Darling Viewer)
ColumnValueList.cs,ApplyTo.) New shared fixture case 11 runs it on both halves.ColumnValueListModel.TicksDirty); a text-only Apply keeps the stored value part. Select All and Tick only now raiseTicksChanged, so the "No values are ticked" note shows at once.Script,ErrorandInfosuffixes.ColumnFilterScope.AllServers) so their filters survive a restart. The drill-down windows get no scope.column-filters.jsonnow lives under Local AppData; a file in the old Roaming folder is moved over once and then gone.Done on the web page
grid-value-filter.js:valueKeyfolds like OrdinalIgnoreCase (fixture cases 12 and 13 pass through the real boxes); the no-list name rule (NO_LIST_SUFFIXES,NO_LIST_FRAGMENTS,noListName,listableColumn) exists;serializeFiltersskips a column flaggedsessionand any column named like prose;parseFiltersdrops such columns. The test harness now finds a value by its exact spelling first.GridValueFilterBehaviourTests: a missing Node fails on CI (GITHUB_ACTIONS is set) instead of skipping. In a recent dev run's log (run 37814992459) no page-test class was skipped (the skips there are PostgreSQL live tests), so this only guards the future.Test plan
*ColumnFilter*Total 52, Failed 0;*ColumnValue*Total 51, Failed 0;*GridValueFilterBehaviour*Total 22, Failed 0.*Census*483/0 (1 skipped),*Hygiene*90/0,*Ratchet*15/0,*Scrub*123/0 (32 skipped),*Guard*338/0 (27 skipped).WebModuleLoadTestswas not run. (run in round 2b: see below)Not done (the web page half and one test)
panels.jsis unchanged: not yet done are (a) usinglistableColumninlisted()and the opt-outvalueList: falseplus the name rule, (b) raw-value cells for the list (blank means a null or empty raw value, whatever the cell renders; documented equivalence for plain text columns), (c) the search folded withvalueKey, (d) writes to localStorage debounced about 300 ms and flushed onpagehide(the harnessstored()then needsflushGridFilters), (e) thesessionflag on a text match for a no-list column, plus pruning loaded entries for avalueList: falsecolumn, (f) the read-only "kept value filter" line with a Clear button in the web popup, (g) FinOps: rewrite the URL to#/finops/{server}/{tab}withhistory.replaceStateinpaint()so the filter key carries the server, with a test. (done in round 2b)NO_LIST_SUFFIXES/NO_LIST_FRAGMENTStoColumnValueListColumns.NameSuffixes/NameFragments, and a fixture-pairs test on both halves (the C# properties exist; the tests are not written). (done in round 2b)HistoryDataGridwindows set none (the persistence behaviour is tested; the pin is not). (done in round 2b)valueList: falsemarker on thereasoncolumn (server-tabs.js:3123). (done in round 2b)Round 2b
The web page half of round 2 is finished and the "Not done" list above is ticked off. All of it is in
panels.jsandgrid-value-filter.jsunless a file is named.Done on the web page
gridFilternow decides withlistableColumn(thevalueList: falsemarker plus the shared name rule), so the AlertsDetailcolumn (detail_text), job step and severe-error messages, recommendation details, descriptions, scripts, errors and info cells get no list even when every cell is short. (panels.js,listed.)listCell.)valueKey, like .NETOrdinalIgnoreCase: a capital sharp s no longer matches a lower-case one.pagehide(flushGridFiltersis exported for the tests).session: it stays through refreshes and tab switches, is never written, and a stored one on such a column is dropped when the grid is drawn.serializeFilterskeeps only the value part of such a column if it still has one.pages/finops.jsrewrites a bare#/finopsto#/finops/{server}/{tab}withhistory.replaceState(pinFinopsHash), before the tab builds, so each server has its own filter key.reasoncolumns. The stored-caveatreasoncolumn (server-tabs.js) and the mute rulereasoncolumn (mute-rules.js, free text typed by a user) are markedvalueList: false. A key ending in "reason" is not in the shared name rule, which the desktop pins to the same list.app.css.New tests (each seen failing with its production line reverted)
ColumnValueListNameRuleTests(suffix and fragment lists equal on both halves; desktop answers for the fixture's name pairs)"info"fromNO_LIST_SUFFIXESGridValueFilterBehaviourTests.TheNoListNameRule_...EveryPairInTheSharedFixture(web half of the pairs)...AColumnNamedLikeProse_OrAStatement_GetsNoList_EvenWithShortCellslistedback tovalueList !== false...TheList_HoldsTheRawValue_...textContentagain...TheValueSearch_FoldsCaseLikeDotNetOrdinalIgnoreCasetoLowerCase...TheKeptCopy_IsWrittenOnceAfterABurst_AndAtOnceOnPagehidepagehidelistener (two reverts)...ATextMatch_OnAColumnWithNoList_LastsTheSessionOnly...merged.session = true...AValuePartKeptOnAColumnThatHasNoList_......OnABareFinOpsAddress_EachServerGetsItsOwnFiltersreplaceStatecall removed...TheFinOpsPaint_PinsTheServerBeforeItBuildsTheTab(source pin: the page cannot run without a server)pinFinopsHashcall removed...TheFreeTextReasonColumns_AreMarkedWithNoList(source pin: those descriptors sit in page modules that need a server)ColumnFilterWiringCensusTests.Alert_History_and_Job_History_set_the_one_all_servers_scope_in_both_apps...The_three_HistoryDataGrid_windows_in_each_app_have_no_scopeThe earlier round's atomic-write test (
A_write_goes_through_a_temp_file_so_a_failed_write_leaves_the_old_file_whole) was also checked: withColumnFilterStorewriting straight into the real file it fails (and so does the retry test), so it does test the temp-file-and-replace path.Test plan
*ColumnFilter*Total 54, Failed 0;*ColumnValue*Total 55, Failed 0;*GridValueFilterBehaviour*Total 32, Failed 0;*GridFilter*Total 15, Failed 0;WebModuleLoadTestsTotal 7, Failed 0 (it caught a trailing comment with the word "tab" infinops.js; moved onto its own line).*Census*485/0 (1 skipped),*Ratchet*15/0,*Hygiene*90/0,*Scrub*123/0 (32 skipped),*Guard*338/0 (27 skipped).*Grid*168/0,*ServerTab*84/0,*Census*63/0,*Scrub*32/0,*Guard*136/0 (noColumnFilter,RatchetorHygieneclasses there).Notes
valueList: falsemarker are session-only, as on the desktop.Round 3
Six fixes from the second check. Each behaviour has a test that fails with its production line reverted. The round 2b line about the two
reasoncolumns carryingvalueList: falseis replaced by item 4 below.panels.js,listCell). A row with no raw field for the column (undefined), under a column withrenderordisplay, uses the shown text, as before the list read raw values. The Alerts Status, Triage and Mute columns list their labels again (Status: Delivered, Delivery error, Muted); unticking "Muted" hides only the Muted rows, and a stored "Muted" filter matches them after a reload. AvalueList: falsecolumn still gets no list. Test:AColumnBuiltFromOtherFields_ListsTheTextItShows_AsTheAlertsStatusColumnDoes(harness scenarioderivedColumn). Reverted: theraw === undefinedbranch; the harness cannot find the "Muted" item and fails.ColumnFilterPopup.xaml.cs): the button now callsApplyFilter(), as the web does, so a click outside or Escape keeps the cleared state. Test:A_stored_value_part_on_a_column_with_no_list_shows_read_only_with_a_way_to_clear_itnow checks that the event fires on the click. Reverted: theApplyFilter()call.mute-rules.js): removed thevalueList: falseon the form'sreasonfield (the rules table never went through a grid), and removed its source-text pin.ColumnValueListColumns.cs,grid-value-filter.js). The explicit marker atserver-tabs.js(stored caveats) is gone: the name rule covers it. The fixture gainedReasonandRecommendationReasonpairs, so both halves are tested; the census tests keep the lists equal. TheofferedByNamescenario no longer sets a marker onreason. Reverted: the C# suffix (suffix-list and pair tests fail) and the JS suffix (AColumnNamedLikeProse_...fails,reasonoffered).ColumnFilterStore.cs): a failed save retries after 1.5 s, then 3, 6, 12, 24, 48 and at most 60 s (a good save resets it;RetryDelayshows the next wait). The file I/O now runs outside the state lock, under its own write lock, soLoadandSavenever wait on the disk; a change made during a write keeps the store dirty. The warning now says what happens: "The save is tried again in N seconds, with longer waits (up to 60 seconds) while it keeps failing, and at the next change." Tests:A_save_that_keeps_failing_backs_off_...(10 s debounce: 10, 20, 40, 60, 60, 60 then reset) andA_slow_write_does_not_hold_up_Load_or_Save(a blocked writer;LoadandSavefinish). Reverted: the doubling; the write moved back under the lock (the second test fails).ColumnFilterStore.cs): the last copy of a server and grid in the file wins, and a column named twice in a grid keeps its last copy, soLoadandSavesee one entry. Test:The_same_server_and_grid_twice_in_a_file_keeps_the_last_copy_so_Load_and_Save_agree(a hand-built file). Reverted: the entry-level removal (the column-level dedupe was not reverted on its own; the same test asserts it).The squash text stays true: it lists no
reasoncolumn and says a kept value filter shows a Clear button, which still holds.Test plan (round 3)
*ColumnFilter*57/0,*ColumnValue*55/0,*GridValueFilterBehaviour*32/0,*GridFilter*15/0,WebModuleLoad*7/0,*Census*485/0 (1 skipped),*Ratchet*15/0,*Hygiene*90/0,*Scrub*123/0 (32 skipped),*Guard*338/0 (27 skipped).*Grid*168/0,*ServerTab*84/0,*Census*63/0,*Scrub*32/0,*Guard*136/0 (noColumnFilter,RatchetorHygieneclasses there).Fixes #5565