Add query parameter support for filter deep-linking#192
Add query parameter support for filter deep-linking#192
Conversation
Co-authored-by: mo-esmp <[email protected]>
Co-authored-by: mo-esmp <[email protected]>
Co-authored-by: mo-esmp <[email protected]>
Co-authored-by: mo-esmp <[email protected]>
|
Great how this seems to have just one-shotted this but i cant comment on react/next a lot here. |
|
@followynne It would be great if you could review the React code when you have time. |
There was a problem hiding this comment.
Pull request overview
Adds deep-linking support by synchronizing Serilog UI filter state with URL query parameters, so filtered views can be shared/bookmarked and survive reloads.
Changes:
- Added query param parsing/serialization utilities for
SearchFormwith alias support. - Added a React hook to initialize filters from the URL and to update the URL as filters change.
- Added unit tests and README documentation for supported parameters.
Reviewed changes
Copilot reviewed 5 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Serilog.Ui.Web/src/app/util/queryParams.ts | Implements URL query parsing/serialization for filter state. |
| src/Serilog.Ui.Web/src/app/hooks/useQueryParamSync.ts | Synchronizes form state with URL query parameters. |
| src/Serilog.Ui.Web/src/app/components/Index.tsx | Enables query-param synchronization hook at app entry. |
| src/Serilog.Ui.Web/src/tests/util/queryParams.spec.ts | Adds unit coverage for parse/serialize behavior. |
| README.md | Documents supported query parameters and examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| | Parameter | Aliases | Description | Example | | ||
| |-----------|---------|-------------|---------| | ||
| | `table` | `key` | The log table/database to query | `?table=MyLogs` | | ||
| | `level` | - | Filter by log level | `?level=Error` | | ||
| | `search` | - | Search text filter | `?search=exception` | | ||
| | `startDate` | `from` | Start date/time (ISO 8601) | `?startDate=2024-01-01T00:00:00Z` | | ||
| | `endDate` | `to`, `till` | End date/time (ISO 8601) | `?endDate=2024-01-02T00:00:00Z` | | ||
| | `sortOn` | - | Property to sort by | `?sortOn=Timestamp` | | ||
| | `sortBy` | - | Sort direction (`Asc` or `Desc`) | `?sortBy=Desc` | | ||
| | `page` | - | Page number | `?page=2` | | ||
| | `count` | `entriesPerPage` | Number of entries per page | `?count=50` | | ||
|
|
There was a problem hiding this comment.
The README’s markdown table is malformed (each row starts with ||), which renders as an extra empty column in most markdown parsers. Use single leading/trailing | for each row so the table formats correctly.
| it('includes table even if empty string', () => { | ||
| const form: SearchForm = { | ||
| ...baseForm, | ||
| table: '', | ||
| }; | ||
|
|
||
| const result = serializeSearchParams(form); | ||
|
|
||
| // Empty string should not be included | ||
| expect(result.get('table')).toBeNull(); | ||
| }); |
There was a problem hiding this comment.
This test case name contradicts its assertions: it says "includes table even if empty string" but the expectation verifies the table param is omitted when table is an empty string. Rename the test (and/or adjust the inline comment) to match the behavior being asserted.
| // Initialize form from URL params on mount | ||
| useEffect(() => { | ||
| if (isInitialized.current || !tableKeys || !isArrayGuard(tableKeys)) return; | ||
|
|
||
| const urlParams = parseSearchParams(searchParams); |
There was a problem hiding this comment.
useQueryParamSync only applies searchParams to the form once (guarded by isInitialized). This means browser back/forward navigation or manually edited query params after initial load will not update the form, despite the hook intending bidirectional sync. Consider handling subsequent searchParams changes when they differ from lastParamsRef.current by re-parsing and updating the form (while still using the ref to avoid loops).
| if (entriesPerPage) { | ||
| const count = parseInt(entriesPerPage, 10); | ||
| // Validate it's a positive number | ||
| if (!isNaN(count) && count > 0) { | ||
| result.entriesPerPage = entriesPerPage; |
There was a problem hiding this comment.
entriesPerPage validation parses the value but then stores the original string. This allows non-integer strings like "20.5" or "10abc" to pass (because parseInt succeeds) and then propagate to the form/URL, even though the UI only supports specific numeric string options (e.g. 10/25/50/100). Consider validating the raw string strictly (digits-only) and/or normalizing to count.toString() and restricting to supported values.
| if (entriesPerPage) { | |
| const count = parseInt(entriesPerPage, 10); | |
| // Validate it's a positive number | |
| if (!isNaN(count) && count > 0) { | |
| result.entriesPerPage = entriesPerPage; | |
| if (entriesPerPage && /^[0-9]+$/.test(entriesPerPage)) { | |
| const count = parseInt(entriesPerPage, 10); | |
| // Validate it's a positive integer and normalize the value | |
| if (!isNaN(count) && count > 0) { | |
| result.entriesPerPage = count.toString(); |
| // Ensure we have at least one table key | ||
| if (tableKeys.length === 0) return; | ||
|
|
||
| const tableKeysDefaultValue = tableKeys[0]; | ||
|
|
||
| // Validate and apply URL params to the form | ||
| Object.entries(urlParams).forEach(([key, value]) => { | ||
| if (value !== undefined && value !== null) { | ||
| // Special handling for table - ensure it's valid |
There was a problem hiding this comment.
isArrayGuard(tableKeys) already guarantees tableKeys.length > 0 (it checks value?.length > 0), so the later if (tableKeys.length === 0) return; is redundant and can be removed to simplify the initialization logic.
| // Ensure we have at least one table key | |
| if (tableKeys.length === 0) return; | |
| const tableKeysDefaultValue = tableKeys[0]; | |
| // Validate and apply URL params to the form | |
| Object.entries(urlParams).forEach(([key, value]) => { | |
| if (value !== undefined && value !== null) { | |
| // Special handling for table - ensure it's valid | |
| const tableKeysDefaultValue = tableKeys[0]; | |
| // Validate and apply URL params to the form | |
| Object.entries(urlParams).forEach(([key, value]) => { | |
| if (value !== undefined && value !== null) { | |
| // Special handling for table - ensure it's valid | |
| Object.entries(urlParams).forEach(([key, value]) => { | |
| if (value !== undefined && value !== null) { | |
| // Special handling for table - ensure it's valid |
Enables linking to serilog-ui with pre-applied filters via URL query parameters. Previously, filter state existed only in-memory and was lost on page reload. External systems and team members can now share direct links to specific logs or timeframes.
Changes
Core Implementation
queryParams.ts: Parser/serializer for URL ↔ SearchForm conversion with validationfrom/startDate,to/till/endDate,key/table)useQueryParamSync.ts: React hook for bidirectional form ↔ URL syncIndex.tsx: Enabled sync hookTests
Documentation
Example Usage
Supported parameters:
table/key,level,search,startDate/from,endDate/to/till,sortOn,sortBy,page,count/entriesPerPageOriginal prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.