[P0] Shareable links with filter state #103

Open
opened 2026-04-01 22:51:32 +02:00 by claude · 2 comments
Owner

Parent Epic

#100

Problem

Incident response requires sharing "look at this" links in Slack/Teams. Currently, every visit starts from scratch — there are no saved views, no shareable URLs with filter state encoded, and no way to bookmark a specific investigation context. Without this, Cameleer can't be part of a team's incident workflow.

Current State

  • URL only encodes the page route (e.g., /exchanges, /dashboard/sample-app)
  • Time range selection not persisted in URL
  • Status filters (OK/Warn/Error/Running) not persisted in URL
  • Exchange selection uses browser history.state — not shareable
  • No "copy link" button anywhere
  • No saved views / presets

Proposed Solution

1. URL-Encoded Filter State

All filter state should be reflected in the URL query string so links are shareable:

/exchanges?status=error&range=1h&app=backend-app
/exchanges?status=error&range=custom&from=2026-04-01T20:00&to=2026-04-01T21:00
/exchanges/backend-app/route3/73A3A974C735725-B3?tab=error
/dashboard/sample-app?range=24h
/runtime/sample-app/cameleer3-sample-54fddccf55-nv5ds-1

URL parameters to encode:

Parameter Example Pages
status ok,error Exchanges
range 1h, 3h, 6h, today, 24h, 7d All
from / to ISO timestamps All (custom range)
app backend-app Already in path
route route3 Already in path
exchange 73A3A...B3 Already in path
tab info, headers, error, timeline Exchange detail
search orderId=123 Exchanges
┌─────────────────────────────────────────────┐
│  All Applications / backend-app   🔗 [Copy Link]  │
│                                              │
│  "Link copied — share it with your team"     │
└─────────────────────────────────────────────┘
  • Add a "Copy Link" button in the breadcrumb bar or top bar
  • Shows a toast confirmation when copied
  • The link includes all current filter state

3. Saved Views (stretch goal)

┌──────────────────────────────────────────────┐
│  Saved Views                     [+ Save Current] │
│──────────────────────────────────────────────│
│  ★  Production Errors (last 24h)    [Load]   │
│  ★  Slow Routes (P99 > 1s)         [Load]   │
│  ★  Backend App Overview            [Load]   │
└──────────────────────────────────────────────┘
  • Save current filter state as a named view
  • Personal saved views stored in localStorage
  • Team-shared saved views stored server-side (future)

Implementation Notes

  • Use React Router's useSearchParams to sync filter state with URL
  • Replace history.state for exchange selection with URL path parameters (already partially done)
  • Ensure browser back/forward still works correctly with URL-based state
  • Deep links should restore exact view state including selected exchange, detail tab, and scroll position

Acceptance Criteria

  • Time range, status filters, and search term reflected in URL query params
  • Opening a shared URL restores the exact view state
  • Copy Link button in top bar copies current URL to clipboard with toast
  • Browser back/forward navigation preserves filter state
  • Exchange detail tab selection reflected in URL
  • Custom time ranges encoded as ISO timestamps in URL

Competitive Reference

  • Datadog: Every view has a shareable URL with full state. Saved views per page.
  • Grafana: Dashboard URLs encode time range, variables, panel focus. Snapshot sharing.
  • Kibana: Share button generates short URL or embed iframe.
## Parent Epic #100 ## Problem Incident response requires sharing "look at this" links in Slack/Teams. Currently, every visit starts from scratch — there are no saved views, no shareable URLs with filter state encoded, and no way to bookmark a specific investigation context. Without this, Cameleer can't be part of a team's incident workflow. ## Current State - URL only encodes the page route (e.g., `/exchanges`, `/dashboard/sample-app`) - Time range selection not persisted in URL - Status filters (OK/Warn/Error/Running) not persisted in URL - Exchange selection uses browser `history.state` — not shareable - No "copy link" button anywhere - No saved views / presets ## Proposed Solution ### 1. URL-Encoded Filter State All filter state should be reflected in the URL query string so links are shareable: ``` /exchanges?status=error&range=1h&app=backend-app /exchanges?status=error&range=custom&from=2026-04-01T20:00&to=2026-04-01T21:00 /exchanges/backend-app/route3/73A3A974C735725-B3?tab=error /dashboard/sample-app?range=24h /runtime/sample-app/cameleer3-sample-54fddccf55-nv5ds-1 ``` **URL parameters to encode:** | Parameter | Example | Pages | |-----------|---------|-------| | `status` | `ok,error` | Exchanges | | `range` | `1h`, `3h`, `6h`, `today`, `24h`, `7d` | All | | `from` / `to` | ISO timestamps | All (custom range) | | `app` | `backend-app` | Already in path | | `route` | `route3` | Already in path | | `exchange` | `73A3A...B3` | Already in path | | `tab` | `info`, `headers`, `error`, `timeline` | Exchange detail | | `search` | `orderId=123` | Exchanges | ### 2. Copy Link Button ``` ┌─────────────────────────────────────────────┐ │ All Applications / backend-app 🔗 [Copy Link] │ │ │ │ "Link copied — share it with your team" │ └─────────────────────────────────────────────┘ ``` - Add a "Copy Link" button in the breadcrumb bar or top bar - Shows a toast confirmation when copied - The link includes all current filter state ### 3. Saved Views (stretch goal) ``` ┌──────────────────────────────────────────────┐ │ Saved Views [+ Save Current] │ │──────────────────────────────────────────────│ │ ★ Production Errors (last 24h) [Load] │ │ ★ Slow Routes (P99 > 1s) [Load] │ │ ★ Backend App Overview [Load] │ └──────────────────────────────────────────────┘ ``` - Save current filter state as a named view - Personal saved views stored in localStorage - Team-shared saved views stored server-side (future) ## Implementation Notes - Use React Router's `useSearchParams` to sync filter state with URL - Replace `history.state` for exchange selection with URL path parameters (already partially done) - Ensure browser back/forward still works correctly with URL-based state - Deep links should restore exact view state including selected exchange, detail tab, and scroll position ## Acceptance Criteria - [ ] Time range, status filters, and search term reflected in URL query params - [ ] Opening a shared URL restores the exact view state - [ ] Copy Link button in top bar copies current URL to clipboard with toast - [ ] Browser back/forward navigation preserves filter state - [ ] Exchange detail tab selection reflected in URL - [ ] Custom time ranges encoded as ISO timestamps in URL ## Competitive Reference - **Datadog:** Every view has a shareable URL with full state. Saved views per page. - **Grafana:** Dashboard URLs encode time range, variables, panel focus. Snapshot sharing. - **Kibana:** Share button generates short URL or embed iframe.
claude added the featureuxpmf labels 2026-04-01 22:51:32 +02:00
Author
Owner

Design Specification

URL Schema

Path = navigation scope, query params = view state. Params are optional (absent = default).

Param Type Default Pages Description
range enum 1h All Time preset: 1h,3h,6h,today,24h,7d,custom
from/to ISO datetime - All Custom range (UTC), required when range=custom
status csv all Exchanges Status: ok,error,warning,running
search string - Exchanges Full-text search
sort string startTime Exchanges Sort column
dir asc/desc desc Exchanges Sort direction
tab enum info Detail Detail panel tab
live 0/1 1 All Auto-refresh

Examples:

/exchanges?range=24h&status=error
/exchanges/backend-app/route3/73A3A974C735725-B3?tab=error&range=24h
/dashboard/sample-app?range=7d&live=0

Architecture: URL as Source of Truth

UrlFilterSyncProvider wraps the existing GlobalFilterProvider from the design system. Inner UrlFilterSync component syncs bidirectionally: URL→state on mount/popstate, state→URL on filter changes (debounced, replaceState). No design system changes needed.

New files:

  • ui/src/hooks/url-filter-types.ts — Types, parse/serialize, mapping tables
  • ui/src/components/UrlFilterSyncProvider.tsx — Wrapper provider
  • ui/src/hooks/useSavedViews.ts — Saved views (stretch)

Modified files:

  • LayoutShell.tsx — Swap provider, add Copy Link button
  • Dashboard.tsx — Rename text to search, add sort/dir URL sync
  • ExchangesPage.tsx — Add tab URL param for detail panel

History Behavior

Filter changes → replaceState (Back undoes navigation, not each filter tweak). Navigation changes (sidebar click, exchange select, tab switch) → pushState.

Global params (range, from, to, live) preserved across sidebar navigation. Page-specific params (status, search, sort, dir, tab) reset.

Breadcrumb area, right-aligned. navigator.clipboard.writeText(window.location.href) with toast confirmation. Keyboard shortcut: Ctrl+Shift+C.

Full sequence: URL → React Router match → auth check (redirect to login if needed, storing URL in sessionStorage) → UrlFilterSync reads params → filters applied → data fetched → view rendered. Invalid params silently dropped (use defaults).

Migration

Existing URLs without params continue to work. ?text= renamed to ?search= (both accepted during transition). Exchange selection migrates from history.state to URL path params (already partially done via urlDerivedExchange).

Rollout (9 independent steps)

  1. Add url-filter-types.ts (pure functions)
  2. Add UrlFilterSyncProvider
  3. Swap provider in LayoutShell
  4. Add Copy Link button
  5. Migrate textsearch in Dashboard
  6. Add sort/dir URL params
  7. Add tab URL param to DetailPanel
  8. Update exchange selection to prefer URL path
  9. Add Saved Views UI (stretch)
## Design Specification ### URL Schema Path = navigation scope, query params = view state. Params are optional (absent = default). | Param | Type | Default | Pages | Description | |-------|------|---------|-------|-------------| | `range` | enum | `1h` | All | Time preset: `1h`,`3h`,`6h`,`today`,`24h`,`7d`,`custom` | | `from`/`to` | ISO datetime | - | All | Custom range (UTC), required when `range=custom` | | `status` | csv | all | Exchanges | Status: `ok,error,warning,running` | | `search` | string | - | Exchanges | Full-text search | | `sort` | string | `startTime` | Exchanges | Sort column | | `dir` | `asc`/`desc` | `desc` | Exchanges | Sort direction | | `tab` | enum | `info` | Detail | Detail panel tab | | `live` | `0`/`1` | `1` | All | Auto-refresh | **Examples:** ``` /exchanges?range=24h&status=error /exchanges/backend-app/route3/73A3A974C735725-B3?tab=error&range=24h /dashboard/sample-app?range=7d&live=0 ``` ### Architecture: URL as Source of Truth `UrlFilterSyncProvider` wraps the existing `GlobalFilterProvider` from the design system. Inner `UrlFilterSync` component syncs bidirectionally: URL→state on mount/popstate, state→URL on filter changes (debounced, replaceState). No design system changes needed. **New files:** - `ui/src/hooks/url-filter-types.ts` — Types, parse/serialize, mapping tables - `ui/src/components/UrlFilterSyncProvider.tsx` — Wrapper provider - `ui/src/hooks/useSavedViews.ts` — Saved views (stretch) **Modified files:** - `LayoutShell.tsx` — Swap provider, add Copy Link button - `Dashboard.tsx` — Rename `text` to `search`, add `sort`/`dir` URL sync - `ExchangesPage.tsx` — Add `tab` URL param for detail panel ### History Behavior Filter changes → `replaceState` (Back undoes navigation, not each filter tweak). Navigation changes (sidebar click, exchange select, tab switch) → `pushState`. Global params (`range`, `from`, `to`, `live`) preserved across sidebar navigation. Page-specific params (`status`, `search`, `sort`, `dir`, `tab`) reset. ### Copy Link Button Breadcrumb area, right-aligned. `navigator.clipboard.writeText(window.location.href)` with toast confirmation. Keyboard shortcut: Ctrl+Shift+C. ### Deep Link Restoration Full sequence: URL → React Router match → auth check (redirect to login if needed, storing URL in sessionStorage) → UrlFilterSync reads params → filters applied → data fetched → view rendered. Invalid params silently dropped (use defaults). ### Migration Existing URLs without params continue to work. `?text=` renamed to `?search=` (both accepted during transition). Exchange selection migrates from `history.state` to URL path params (already partially done via `urlDerivedExchange`). ### Rollout (9 independent steps) 1. Add `url-filter-types.ts` (pure functions) 2. Add `UrlFilterSyncProvider` 3. Swap provider in LayoutShell 4. Add Copy Link button 5. Migrate `text` → `search` in Dashboard 6. Add `sort`/`dir` URL params 7. Add `tab` URL param to DetailPanel 8. Update exchange selection to prefer URL path 9. Add Saved Views UI (stretch)
Author
Owner

Conflict Analysis: URL Filter State vs Current Navigation Infrastructure

After a thorough review of the current sidebar, useScope, LayoutShell, and ExchangesPage code against this proposed design, there are 5 real conflicts that need to be addressed before implementation. The sidebar component itself is safe (it only reads location.pathname and location.state.sidebarReveal), but the navigation layer connecting sidebar clicks to page URLs is where all conflicts live.


Conflict 1: All Navigation Methods Strip Query Params (HIGH)

Every navigation function in useScope.ts builds bare URLs with no query string:

// useScope.ts — tab switch
navigate(`/${newTab}/${scope.appId}/${scope.routeId}`);  // query params GONE

// useScope.ts — app select
navigate(`/${tab}/${appId}`);  // query params GONE

Same in LayoutShell.tsx — sidebar click handler:

navigate(`/${scope.tab}/${sAppId}/${sRouteId}`, { state });  // query params GONE

Impact: Every sidebar click, tab switch, and app/route navigation wipes all URL-encoded filter state (range, status, search, live, etc.). The spec says "global params preserved across sidebar navigation" — this requires every navigation call to carry forward global query params.

Conflict 2: replaceState Will Clobber location.state (HIGH)

The spec says filter changes use replaceState. But location.state carries critical data that must survive:

  • sidebarReveal — sidebar uses this to highlight the active tree node after LayoutShell translates /apps/X/exchanges/X
  • selectedExchangeExchangesPage uses this to restore exchange selection on Back/Forward in split view

If UrlFilterSync does navigate(url, { replace: true }) without forwarding existing state, changing a time range filter will break sidebar highlighting and lose the selected exchange.

Conflict 3: Auto-Refresh Time Sliding vs URL (MEDIUM)

GlobalFilterProvider auto-slides the time window every 10 seconds when autoRefresh is on with a preset. If time range moves to URL as from/to timestamps, this creates a replaceState every 10 seconds that must preserve all other query params and location.state without URL bar flicker.

Recommendation: URL should only store the preset name (?range=1h), not computed from/to, unless range=custom. The provider computes the actual window from the preset. Auto-refresh with custom ranges should not update the URL.

Conflict 4: handlePaletteSubmit Uses ?text= (LOW)

LayoutShell.tsx line 269:

navigate(`${baseParts.join('/')}?text=${encodeURIComponent(query)}`);

This constructs a fresh URL, stripping any existing filter query params. Needs to use ?search= and merge with existing global params.

Conflict 5: Dashboard Reads ?text= Directly (LOW)

Dashboard uses searchParams.get('text') for its filter. Needs migration to search with backward compat during transition.


Design Gaps in the Current Spec

  1. No mention of sidebarReveal state preservation. The spec discusses pushState vs replaceState but doesn't acknowledge the existing location.state payloads (sidebarReveal, selectedExchange).

  2. No mention of useScope modifications. The spec lists LayoutShell.tsx as modified but doesn't mention useScope.ts — yet useScope is the primary navigation mechanism and it MUST preserve global query params.

  3. The "9 independent steps" rollout isn't independent. Step 3 (swap provider in LayoutShell) breaks things unless navigation methods are updated first. The rollout ordering needs revision.


Conclusion

This issue requires a broader navigation layer redesign rather than just wrapping the existing GlobalFilterProvider. The core navigation functions (useScope, handleSidebarNavigate, handlePaletteSubmit, handlePaletteSelect) all need to become filter-aware, and location.state must be preserved across all replaceState operations. The original spec underestimated the coupling between filter state, navigation, and the sidebar/split-view infrastructure.

The design spec should be revised to address these conflicts before implementation begins.

## Conflict Analysis: URL Filter State vs Current Navigation Infrastructure After a thorough review of the current sidebar, `useScope`, `LayoutShell`, and `ExchangesPage` code against this proposed design, there are **5 real conflicts** that need to be addressed before implementation. The sidebar component itself is safe (it only reads `location.pathname` and `location.state.sidebarReveal`), but the **navigation layer connecting sidebar clicks to page URLs** is where all conflicts live. --- ### Conflict 1: All Navigation Methods Strip Query Params (HIGH) Every navigation function in `useScope.ts` builds bare URLs with no query string: ```typescript // useScope.ts — tab switch navigate(`/${newTab}/${scope.appId}/${scope.routeId}`); // query params GONE // useScope.ts — app select navigate(`/${tab}/${appId}`); // query params GONE ``` Same in `LayoutShell.tsx` — sidebar click handler: ```typescript navigate(`/${scope.tab}/${sAppId}/${sRouteId}`, { state }); // query params GONE ``` **Impact:** Every sidebar click, tab switch, and app/route navigation wipes all URL-encoded filter state (`range`, `status`, `search`, `live`, etc.). The spec says "global params preserved across sidebar navigation" — this requires **every navigation call** to carry forward global query params. ### Conflict 2: `replaceState` Will Clobber `location.state` (HIGH) The spec says filter changes use `replaceState`. But `location.state` carries critical data that must survive: - **`sidebarReveal`** — sidebar uses this to highlight the active tree node after LayoutShell translates `/apps/X` → `/exchanges/X` - **`selectedExchange`** — `ExchangesPage` uses this to restore exchange selection on Back/Forward in split view If `UrlFilterSync` does `navigate(url, { replace: true })` without forwarding existing state, changing a time range filter will break sidebar highlighting and lose the selected exchange. ### Conflict 3: Auto-Refresh Time Sliding vs URL (MEDIUM) `GlobalFilterProvider` auto-slides the time window every 10 seconds when `autoRefresh` is on with a preset. If time range moves to URL as `from`/`to` timestamps, this creates a `replaceState` every 10 seconds that must preserve all other query params and `location.state` without URL bar flicker. **Recommendation:** URL should only store the **preset name** (`?range=1h`), not computed `from`/`to`, unless `range=custom`. The provider computes the actual window from the preset. Auto-refresh with custom ranges should not update the URL. ### Conflict 4: `handlePaletteSubmit` Uses `?text=` (LOW) `LayoutShell.tsx` line 269: ```typescript navigate(`${baseParts.join('/')}?text=${encodeURIComponent(query)}`); ``` This constructs a fresh URL, stripping any existing filter query params. Needs to use `?search=` and merge with existing global params. ### Conflict 5: Dashboard Reads `?text=` Directly (LOW) Dashboard uses `searchParams.get('text')` for its filter. Needs migration to `search` with backward compat during transition. --- ## Design Gaps in the Current Spec 1. **No mention of `sidebarReveal` state preservation.** The spec discusses `pushState` vs `replaceState` but doesn't acknowledge the existing `location.state` payloads (`sidebarReveal`, `selectedExchange`). 2. **No mention of `useScope` modifications.** The spec lists `LayoutShell.tsx` as modified but doesn't mention `useScope.ts` — yet `useScope` is the primary navigation mechanism and it MUST preserve global query params. 3. **The "9 independent steps" rollout isn't independent.** Step 3 (swap provider in LayoutShell) breaks things unless navigation methods are updated first. The rollout ordering needs revision. --- ## Conclusion This issue requires a broader **navigation layer redesign** rather than just wrapping the existing `GlobalFilterProvider`. The core navigation functions (`useScope`, `handleSidebarNavigate`, `handlePaletteSubmit`, `handlePaletteSelect`) all need to become filter-aware, and `location.state` must be preserved across all `replaceState` operations. The original spec underestimated the coupling between filter state, navigation, and the sidebar/split-view infrastructure. The design spec should be revised to address these conflicts before implementation begins.
Sign in to join this conversation.