Skip to content

Commit 5ac232a

Browse files
committed
Enforce limited view segment on dashboard
1 parent 32652d5 commit 5ac232a

4 files changed

Lines changed: 112 additions & 52 deletions

File tree

assets/js/dashboard.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { createAppRouter } from './dashboard/router'
77
import ErrorBoundary from './dashboard/error/error-boundary'
88
import * as api from './dashboard/api'
99
import * as timer from './dashboard/util/realtime-update-timer'
10-
import { redirectForLegacyParams } from './dashboard/util/url-search-params'
10+
import { maybeDoFERedirect } from './dashboard/util/url-search-params'
1111
import SiteContextProvider, {
1212
parseSiteFromDataset
1313
} from './dashboard/site-context'
@@ -40,8 +40,17 @@ if (container && container.dataset) {
4040
api.setSharedLinkAuth(sharedLinkAuth)
4141
}
4242

43+
const limitedToSegmentId = parseLimitedToSegmentId(container.dataset)
44+
const preloadedSegments = parsePreloadedSegments(container.dataset)
45+
4346
try {
44-
redirectForLegacyParams(window.location, window.history)
47+
maybeDoFERedirect(
48+
window.location,
49+
window.history,
50+
limitedToSegmentId === null
51+
? null
52+
: preloadedSegments.find((s) => s.id === limitedToSegmentId) || null
53+
)
4554
} catch (e) {
4655
console.error('Error redirecting in a backwards compatible way', e)
4756
}
@@ -84,8 +93,8 @@ if (container && container.dataset) {
8493
}
8594
>
8695
<SegmentsContextProvider
87-
limitedToSegmentId={parseLimitedToSegmentId(container.dataset)}
88-
preloadedSegments={parsePreloadedSegments(container.dataset)}
96+
limitedToSegmentId={limitedToSegmentId}
97+
preloadedSegments={preloadedSegments}
8998
>
9099
<RouterProvider router={router} />
91100
</SegmentsContextProvider>

assets/js/dashboard/navigation/use-app-navigate.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
LinkProps
1010
} from 'react-router-dom'
1111
import { parseSearch, stringifySearch } from '../util/url-search-params'
12+
import { useSegmentsContext } from '../filtering/segments-context'
13+
import { getSearchToSetSegmentFilter } from '../filtering/segments'
1214

1315
export type AppNavigationTarget = {
1416
/**
@@ -44,11 +46,25 @@ const getNavigateToOptions = (
4446

4547
export const useGetNavigateOptions = () => {
4648
const location = useLocation()
49+
const { limitedToSegmentId, segments } = useSegmentsContext()
50+
const limitedToSegment = segments.find((s) => s.id === limitedToSegmentId)
51+
4752
const getToOptions = useCallback(
4853
({ path, params, search }: AppNavigationTarget) => {
49-
return getNavigateToOptions(location.search, { path, params, search })
54+
const wrappedSearch: typeof search = (searchRecord) => {
55+
const updatedSearchRecord =
56+
typeof search === 'function' ? search(searchRecord) : searchRecord
57+
return limitedToSegment
58+
? getSearchToSetSegmentFilter(limitedToSegment)(updatedSearchRecord)
59+
: updatedSearchRecord
60+
}
61+
return getNavigateToOptions(location.search, {
62+
path,
63+
params,
64+
search: wrappedSearch
65+
})
5066
},
51-
[location.search]
67+
[location.search, limitedToSegment]
5268
)
5369
return getToOptions
5470
}

assets/js/dashboard/util/url-search-params.test.ts

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Filter } from '../query'
22
import {
33
encodeURIComponentPermissive,
4+
getSearchWithEnforcedSegment,
45
isSearchEntryDefined,
5-
getRedirectTarget,
6+
maybeGetLatestReadableSearch,
67
parseFilter,
78
parseLabelsEntry,
89
parseSearch,
@@ -206,57 +207,45 @@ describe(`${stringifySearch.name}`, () => {
206207
})
207208
})
208209

209-
describe(`${getRedirectTarget.name}`, () => {
210+
describe(`${maybeGetLatestReadableSearch.name}`, () => {
210211
it.each([
211212
[''],
212213
['?auth=_Y6YOjUl2beUJF_XzG1hk&theme=light&background=%23ee00ee'],
213214
['?keybindHint=Escape&with_imported=true'],
214215
['?f=is,page,/blog/:category/:article-name&date=2024-10-10&period=day'],
215216
['?f=is,country,US&l=US,United%20States']
216-
])('for modern search %p returns null', (search) => {
217-
expect(
218-
getRedirectTarget({
219-
pathname: '/example.com%2Fdeep%2Fpath',
220-
search
221-
} as Location)
222-
).toBeNull()
217+
])('for modern search string %p returns null', (search) => {
218+
expect(maybeGetLatestReadableSearch(search)).toBeNull()
223219
})
224220

225-
it('returns updated URL for jsonurl style filters (v2), and running the updated value through the function again returns null (no redirect loop)', () => {
226-
const pathname = '/'
221+
it('returns updated search string for jsonurl style filters (v2), and running the updated value through the function again returns null (no redirect loop)', () => {
227222
const search =
228223
'?filters=((is,exit_page,(/plausible.io)),(is,source,(Brave)),(is,city,(993800)))&labels=(993800:Johannesburg)'
229224
const expectedUpdatedSearch =
230225
'?f=is,exit_page,/plausible.io&f=is,source,Brave&f=is,city,993800&l=993800,Johannesburg&r=v2'
231-
expect(
232-
getRedirectTarget({
233-
pathname,
234-
search
235-
} as Location)
236-
).toEqual(`${pathname}${expectedUpdatedSearch}`)
237-
expect(
238-
getRedirectTarget({
239-
pathname,
240-
search: expectedUpdatedSearch
241-
} as Location)
242-
).toBeNull()
226+
expect(maybeGetLatestReadableSearch(search)).toEqual(expectedUpdatedSearch)
227+
expect(maybeGetLatestReadableSearch(expectedUpdatedSearch)).toBeNull()
243228
})
244229

245-
it('returns updated URL for page=... style filters (v1), and running the updated value through the function again returns null (no redirect loop)', () => {
246-
const pathname = '/'
230+
it('returns updated search string for page=... style filters (v1), and running the updated value through the function again returns null (no redirect loop)', () => {
247231
const search = '?page=/docs'
248232
const expectedUpdatedSearch = '?f=is,page,/docs&r=v1'
233+
expect(maybeGetLatestReadableSearch(search)).toEqual(expectedUpdatedSearch)
234+
expect(maybeGetLatestReadableSearch(expectedUpdatedSearch)).toBeNull()
235+
})
236+
})
237+
238+
describe(`${getSearchWithEnforcedSegment.name}`, () => {
239+
it('adds enforced segment appropriately, and running the updated value through the function again returns the same value', () => {
240+
const segment = { id: 100, name: 'Eastern Europe' }
241+
const search = '?auth=foo&embed=true'
242+
const expectedUpdatedSearch =
243+
'?f=is,segment,100&l=s-100,Eastern%20Europe&auth=foo&embed=true'
244+
expect(getSearchWithEnforcedSegment(search, segment)).toEqual(
245+
expectedUpdatedSearch
246+
)
249247
expect(
250-
getRedirectTarget({
251-
pathname,
252-
search
253-
} as Location)
254-
).toEqual(`${pathname}${expectedUpdatedSearch}`)
255-
expect(
256-
getRedirectTarget({
257-
pathname,
258-
search: expectedUpdatedSearch
259-
} as Location)
260-
).toBeNull()
248+
getSearchWithEnforcedSegment(expectedUpdatedSearch, segment)
249+
).toEqual(expectedUpdatedSearch)
261250
})
262251
})

assets/js/dashboard/util/url-search-params.ts

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
getSearchToSetSegmentFilter,
3+
SavedSegment
4+
} from '../filtering/segments'
15
import { Filter, FilterClauseLabels } from '../query'
26
import { v1 } from './url-search-params-v1'
37
import { v2 } from './url-search-params-v2'
@@ -230,8 +234,10 @@ function isAlreadyRedirected(searchParams: URLSearchParams) {
230234
The purpose of this function is to redirect users from one of the previous versions to the current version,
231235
so previous dashboard links still work.
232236
*/
233-
export function getRedirectTarget(windowLocation: Location): null | string {
234-
const searchParams = new URLSearchParams(windowLocation.search)
237+
export function maybeGetLatestReadableSearch(
238+
searchString: string
239+
): null | string {
240+
const searchParams = new URLSearchParams(searchString)
235241
if (isAlreadyRedirected(searchParams)) {
236242
return null
237243
}
@@ -242,27 +248,67 @@ export function getRedirectTarget(windowLocation: Location): null | string {
242248

243249
const isV2 = v2.isV2(searchParams)
244250
if (isV2) {
245-
return `${windowLocation.pathname}${stringifySearch({ ...v2.parseSearch(windowLocation.search), [REDIRECTED_SEARCH_PARAM_NAME]: 'v2' })}`
251+
return stringifySearch({
252+
...v2.parseSearch(searchString),
253+
[REDIRECTED_SEARCH_PARAM_NAME]: 'v2'
254+
})
246255
}
247256

248-
const searchRecord = v2.parseSearch(windowLocation.search)
257+
const searchRecord = v2.parseSearch(searchString)
249258
const isV1 = v1.isV1(searchRecord)
250259

251260
if (!isV1) {
252261
return null
253262
}
254263

255-
return `${windowLocation.pathname}${stringifySearch({ ...v1.parseSearchRecord(searchRecord), [REDIRECTED_SEARCH_PARAM_NAME]: 'v1' })}`
264+
return stringifySearch({
265+
...v1.parseSearchRecord(searchRecord),
266+
[REDIRECTED_SEARCH_PARAM_NAME]: 'v1'
267+
})
268+
}
269+
270+
/**
271+
* It's possible to set a particular segment to be always applied on the data on dashboards accessed with a shared link.
272+
* This function ensures that the particular segment filter is set to the URL string on initial page load.
273+
* Other functions ensure that it can't be removed.
274+
*/
275+
export function getSearchWithEnforcedSegment(
276+
searchString: string,
277+
enforcedSegment: Pick<SavedSegment, 'id' | 'name'>
278+
): string {
279+
const searchRecord = parseSearch(searchString)
280+
return stringifySearch(
281+
getSearchToSetSegmentFilter(enforcedSegment)(searchRecord)
282+
)
256283
}
257284

258285
/** Called once before React app mounts. If legacy url search params are present, does a redirect to new format. */
259-
export function redirectForLegacyParams(
286+
export function maybeDoFERedirect(
260287
windowLocation: Location,
261-
windowHistory: History
288+
windowHistory: History,
289+
enforcedSegment: Pick<SavedSegment, 'id' | 'name'> | null
262290
) {
263-
const redirectTargetURL = getRedirectTarget(windowLocation)
264-
if (redirectTargetURL === null) {
291+
const originalSearchString = windowLocation.search
292+
293+
let updatedSearchString = maybeGetLatestReadableSearch(originalSearchString)
294+
295+
if (enforcedSegment) {
296+
updatedSearchString = getSearchWithEnforcedSegment(
297+
updatedSearchString ?? originalSearchString,
298+
enforcedSegment
299+
)
300+
}
301+
302+
if (
303+
updatedSearchString === null ||
304+
updatedSearchString === originalSearchString
305+
) {
265306
return
266307
}
267-
windowHistory.pushState({}, '', redirectTargetURL)
308+
309+
windowHistory.pushState(
310+
{},
311+
'',
312+
`${windowLocation.pathname}${updatedSearchString}`
313+
)
268314
}

0 commit comments

Comments
 (0)