Skip to content

Commit

Permalink
Serialize filters and labels as ?f=is,page,/a,/b&f=...
Browse files Browse the repository at this point in the history
  • Loading branch information
apata committed Jan 9, 2025
1 parent 50eef62 commit e81fc73
Show file tree
Hide file tree
Showing 11 changed files with 653 additions and 268 deletions.
2 changes: 1 addition & 1 deletion assets/js/dashboard/nav-menu/filters-bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import userEvent from '@testing-library/user-event'
import { TestContextProviders } from '../../../test-utils/app-context-providers'
import { FiltersBar, handleVisibility } from './filters-bar'
import { getRouterBasepath } from '../router'
import { stringifySearch } from '../util/url'
import { stringifySearch } from '../util/url-search-params'

const domain = 'dummy.site'

Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/navigation/use-app-navigate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
NavigateOptions,
LinkProps
} from 'react-router-dom'
import { parseSearch, stringifySearch } from '../util/url'
import { parseSearch, stringifySearch } from '../util/url-search-params'

export type AppNavigationTarget = {
/**
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/query-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useLocation } from 'react-router'
import { useMountedEffect } from './custom-hooks'
import * as api from './api'
import { useSiteContext } from './site-context'
import { parseSearch } from './util/url'
import { parseSearch } from './util/url-search-params'
import dayjs from 'dayjs'
import { nowForSite, yesterday } from './util/date'
import {
Expand Down
2 changes: 1 addition & 1 deletion assets/js/dashboard/query-dates.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import DatePicker from './datepicker'
import { TestContextProviders } from '../../test-utils/app-context-providers'
import { stringifySearch } from './util/url'
import { stringifySearch } from './util/url-search-params'
import { useNavigate } from 'react-router-dom'
import { getRouterBasepath } from './router'

Expand Down
58 changes: 58 additions & 0 deletions assets/js/dashboard/query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/** @format */

import { maybeGetRedirectTargetFromLegacySearchParams } from './query'

describe(`${maybeGetRedirectTargetFromLegacySearchParams.name}`, () => {
it.each([
[''],
['?auth=_Y6YOjUl2beUJF_XzG1hk&theme=light&background=%23ee00ee'],
['?keybindHint=Escape&with_imported=true'],
['?f=is,page,/blog/:category/:article-name&date=2024-10-10&period=day'],
['?f=is,country,US&l=US,United%20States']
])('for modern search %p returns null', (search) => {
expect(
maybeGetRedirectTargetFromLegacySearchParams({
pathname: '/example.com%2Fdeeper',
search
} as Location)
).toBeNull()
})

it('returns updated URL for jsonurl style filters, and running the updated value through the function again returns null (no redirect loop)', () => {
const pathname = '/'
const search =
'?filters=((is,exit_page,(/plausible.io)),(is,source,(Brave)),(is,city,(993800)))&labels=(993800:Johannesburg)'
const expectedUpdatedSearch =
'?f=is,exit_page,/plausible.io&f=is,source,Brave&f=is,city,993800&l=993800,Johannesburg'
expect(
maybeGetRedirectTargetFromLegacySearchParams({
pathname,
search
} as Location)
).toEqual(`${pathname}${expectedUpdatedSearch}`)
expect(
maybeGetRedirectTargetFromLegacySearchParams({
pathname,
search: expectedUpdatedSearch
} as Location)
).toBeNull()
})

it('returns updated URL for page=... style filters, and running the updated value through the function again returns null (no redirect loop)', () => {
const pathname = '/'
const search = '?page=/docs'
const expectedUpdatedSearch = '?f=is,page,/docs'
expect(
maybeGetRedirectTargetFromLegacySearchParams({
pathname,
search
} as Location)
).toEqual(`${pathname}${expectedUpdatedSearch}`)
expect(
maybeGetRedirectTargetFromLegacySearchParams({
pathname,
search: expectedUpdatedSearch
} as Location)
).toBeNull()
})
})
73 changes: 48 additions & 25 deletions assets/js/dashboard/query.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** @format */

import { parseSearch, stringifySearch } from './util/url'
import {
nowForSite,
formatISO,
Expand All @@ -20,6 +19,11 @@ import { PlausibleSite } from './site-context'
import { ComparisonMode, QueryPeriod } from './query-time-periods'
import { AppNavigationTarget } from './navigation/use-app-navigate'
import { Dayjs } from 'dayjs'
import { legacyParseSearch } from './util/legacy-jsonurl-url-search-params'
import {
FILTER_URL_PARAM_NAME,
stringifySearch
} from './util/url-search-params'

export type FilterClause = string | number

Expand All @@ -37,7 +41,7 @@ export type Filter = [FilterOperator, FilterKey, FilterClause[]]
* for filters `[["is", "city", [2761369]], ["is", "country", ["AT"]]]`,
* labels would be `{"2761369": "Vienna", "AT": "Austria"}`
* */
export type FilterClauseLabels = Record<string, unknown>
export type FilterClauseLabels = Record<string, string>

export const queryDefaultValue = {
period: '30d' as QueryPeriod,
Expand Down Expand Up @@ -100,33 +104,46 @@ export function postProcessFilters(filters: Array<Filter>): Array<Filter> {
})
}

// Called once when dashboard is loaded load. Checks whether old filter style is used and if so,
// updates the filters and updates location
export function filtersBackwardsCompatibilityRedirect(
windowLocation: Location,
windowHistory: History
) {
const searchRecord = parseSearch(windowLocation.search)
const getValue = (k: string) => searchRecord[k]
export function maybeGetRedirectTargetFromLegacySearchParams(
windowLocation: Location
): null | string {
const searchParams = new URLSearchParams(windowLocation.search)
const isCurrentVersion = searchParams.get(FILTER_URL_PARAM_NAME)
if (isCurrentVersion) {
return null
}

// New filters are used - no need to do anything
if (getValue('filters')) {
return
const isJsonURLVersion = searchParams.get('filters')
if (isJsonURLVersion) {
return `${windowLocation.pathname}${stringifySearch(legacyParseSearch(windowLocation.search))}`
}

const searchRecord = legacyParseSearch(windowLocation.search)
const searchRecordEntries = Object.entries(
legacyParseSearch(windowLocation.search)
)

const isBeforeJsonURLVersion = searchRecordEntries.some(
([k]) => k === 'props' || LEGACY_URL_PARAMETERS.hasOwnProperty(k)
)

if (!isBeforeJsonURLVersion) {
return null
}

const changedSearchRecordEntries = []
const filters: DashboardQuery['filters'] = []
let labels: DashboardQuery['labels'] = {}

for (const [key, value] of Object.entries(searchRecord)) {
for (const [key, value] of searchRecordEntries) {
if (LEGACY_URL_PARAMETERS.hasOwnProperty(key)) {
const filter = parseLegacyFilter(key, value) as Filter
filters.push(filter)
const labelsKey: string | null | undefined =
LEGACY_URL_PARAMETERS[key as keyof typeof LEGACY_URL_PARAMETERS]
if (labelsKey && getValue(labelsKey)) {
if (labelsKey && searchRecord[labelsKey]) {
const clauses = filter[2]
const labelsValues = (getValue(labelsKey) as string)
const labelsValues = (searchRecord[labelsKey] as string)
.split('|')
.filter((label) => !!label)
const newLabels = Object.fromEntries(
Expand All @@ -140,18 +157,24 @@ export function filtersBackwardsCompatibilityRedirect(
}
}

if (getValue('props')) {
filters.push(...(parseLegacyPropsFilter(getValue('props')) as Filter[]))
if (searchRecord['props']) {
filters.push(...(parseLegacyPropsFilter(searchRecord['props']) as Filter[]))
}
changedSearchRecordEntries.push(['filters', filters], ['labels', labels])
return `${windowLocation.pathname}${stringifySearch(Object.fromEntries(changedSearchRecordEntries))}`
}

if (filters.length > 0) {
changedSearchRecordEntries.push(['filters', filters], ['labels', labels])
windowHistory.pushState(
{},
'',
`${windowLocation.pathname}${stringifySearch(Object.fromEntries(changedSearchRecordEntries))}`
)
/** Called once before React app mounts. If legacy url search params are present, does a redirect to new format. */
export function filtersBackwardsCompatibilityRedirect(
windowLocation: Location,
windowHistory: History
) {
const redirectTargetURL =
maybeGetRedirectTargetFromLegacySearchParams(windowLocation)
if (redirectTargetURL === null) {
return
}
windowHistory.pushState({}, '', redirectTargetURL)
}

// Returns a boolean indicating whether the given query includes a
Expand Down
Loading

0 comments on commit e81fc73

Please sign in to comment.