Skip to content

Commit 4ddc74e

Browse files
committed
fix(doop): convert old url state to new url state
1 parent 8c0e084 commit 4ddc74e

6 files changed

Lines changed: 289 additions & 49 deletions

File tree

apps/doop/src/App.tsx

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import React, { StrictMode, useLayoutEffect } from "react"
77

88
import { AppShellProvider, ContentHeading } from "@cloudoperators/juno-ui-components"
99
import { RouterProvider, createBrowserHistory, createHashHistory, createRouter } from "@tanstack/react-router"
10-
import { decodeV2, encodeV2 } from "@cloudoperators/juno-url-state-provider"
10+
import { decodeV2, encodeV2, registerConsumer } from "@cloudoperators/juno-url-state-provider"
1111
import styles from "./styles.css?inline"
1212
import { MessagesProvider } from "@cloudoperators/juno-messages-provider"
1313
import StoreProvider from "./components/StoreProvider"
14-
import AsyncWorker from "./components/AsyncWorker"
1514
import { AppShell } from "@cloudoperators/juno-ui-components"
1615
import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
1716
import { useGlobalsActions } from "./components/StoreProvider"
1817
import { routeTree } from "./routeTree.gen"
18+
import { convertAppStateToUrlState, extractSearchStringFromHashFragment, readLegacyUrlState } from "./lib/urlStateUtils"
1919

2020
// Create a new router instance
2121
const router = createRouter({
@@ -41,8 +41,11 @@ export type AppProps = {
4141
basePath?: string
4242
enableHashedRouting?: boolean
4343
showDebugSeverities?: boolean
44+
initialFilters?: Record<string, any>
4445
}
4546

47+
const urlStateManager = registerConsumer("doop")
48+
4649
const App = (props: AppProps = {}) => {
4750
// @ts-expect-error TS(2339) FIXME: Property 'setEndpoint' does not exist on type '{}'.
4851
const { setEndpoint } = useGlobalsActions()
@@ -69,25 +72,30 @@ const App = (props: AppProps = {}) => {
6972
history: props.enableHashedRouting ? createHashHistory() : createBrowserHistory(),
7073
stringifySearch: encodeV2,
7174
parseSearch: (searchString) => {
72-
if (!props.enableHashedRouting) {
73-
return decodeV2(searchString)
74-
}
75+
// If the app is using hashed routing, we need to correctly extract the search string from the hash fragment
76+
const searchStringToDecode = !props.enableHashedRouting
77+
? searchString
78+
: extractSearchStringFromHashFragment(searchString)
79+
80+
// If the search string is empty, return an empty object
81+
if (!searchStringToDecode) return {}
7582

76-
/*
77-
* In case of hashed routing Tanstack router returns URL search params of the entire URL rather than just from the hashed part.
78-
* We'll have to extract the query part from the hash because otherwise in embedded mode the app will be taking search params from the shell app as well.
79-
* Sanitize the search string by extracting the substring between the first '?' and the next '?' (if any), keeping the first '?'.
80-
* https://github.com/TanStack/router/issues/4370
81-
* http://localhost:3000/?preHashParam=prehashtest#/services?postHashParam1=test1?preHashParam=prehashtest
82-
* searchString = "?postHashParam1=test1?preHashParam=prehashtest"
83-
* searchStringFromHash = "?postHashParam1=test1"
83+
/**
84+
* To make new URL state compatible with the legacy URL state,
85+
* we need to extract the legacy URL state from the search string
86+
* and convert it to the new URL state format.
8487
*/
85-
const postHashParams = searchString.indexOf("?")
86-
if (postHashParams === -1) return {} // If no query part is found, return an empty object
87-
const preHashParams = searchString.indexOf("?", postHashParams + 1)
88-
const searchStringFromHash = searchString.slice(postHashParams, preHashParams === -1 ? undefined : preHashParams)
88+
const searchParams = new URLSearchParams(searchStringToDecode)
89+
const legacyUrlState = searchParams.get("__s") // This is used to extract the search params from the hash fragment
90+
let newUrlState = {}
91+
if (legacyUrlState !== null) {
92+
newUrlState = convertAppStateToUrlState(readLegacyUrlState(urlStateManager.currentState()))
93+
searchParams.delete("__s") // Remove the old state from the search params
94+
}
95+
96+
const searchStringWithoutLegacyUrlState = searchParams.toString()
8997

90-
return decodeV2(searchStringFromHash)
98+
return { ...decodeV2(searchStringWithoutLegacyUrlState), ...newUrlState, legacyUrlState }
9199
},
92100
})
93101

@@ -103,7 +111,6 @@ const App = (props: AppProps = {}) => {
103111
// @ts-expect-error TS(2339) FIXME: Property 'displayName' does not exist on type '{}'... Remove this comment to see the full error message
104112
heading={`Decentralized Observer of Policies ${props.displayName ? ` - ${props.displayName}` : ""}`}
105113
/>
106-
<AsyncWorker consumerId={props.id || "doop"} />
107114
<QueryClientProvider client={queryClient}>
108115
<StrictMode>
109116
<RouterProvider basepath={props.basePath || "/"} router={router} />

apps/doop/src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
export const ACTIVE_FILTERS_PREFIX = "f_"

apps/doop/src/lib/urlStateUtils.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
const ACTIVE_FILTERS = "f"
7+
const SEARCH_TERM = "s"
8+
const DETAILS_VIOLATION_GROUP = "v"
9+
10+
export const readLegacyUrlState = (state: any) => {
11+
const activeFilters = state?.[ACTIVE_FILTERS]
12+
const searchTerm = state?.[SEARCH_TERM]
13+
const violationGroup = state?.[DETAILS_VIOLATION_GROUP]
14+
15+
return {
16+
activeFilters,
17+
searchTerm,
18+
violationGroup,
19+
}
20+
}
21+
22+
export const extractSearchStringFromHashFragment = (searchString: string) => {
23+
/*
24+
* In case of hashed routing Tanstack router returns URL search params of the entire URL rather than just from the hashed part.
25+
* We'll have to extract the query part from the hash because otherwise in embedded mode the app will be taking search params from the shell app as well.
26+
* Sanitize the search string by extracting the substring between the first '?' and the next '?' (if any), keeping the first '?'.
27+
* https://github.com/TanStack/router/issues/4370
28+
* http://localhost:3000/?preHashParam=prehashtest#/services?postHashParam1=test1?preHashParam=prehashtest
29+
* searchString = "?postHashParam1=test1?preHashParam=prehashtest"
30+
* searchStringFromHash = "?postHashParam1=test1"
31+
*/
32+
const postHashParams = searchString.indexOf("?")
33+
if (postHashParams === -1) return "" // If no query part is found, return an empty object
34+
const preHashParams = searchString.indexOf("?", postHashParams + 1)
35+
return searchString.slice(postHashParams, preHashParams === -1 ? undefined : preHashParams)
36+
}
37+
38+
export const getFiltersForUrl = (prefix: string, filters: any) =>
39+
filters.reduce((acc: Record<string, string | string[]>, filter: any) => {
40+
// if the filter key already exists, convert the value to an array and add the new value
41+
if (acc[`${prefix}${filter.key}`]) {
42+
if (Array.isArray(acc[`${prefix}${filter.key}`])) {
43+
;(acc[`${prefix}${filter.key}`] as string[]).push(filter.value)
44+
} else {
45+
acc[`${prefix}${filter.key}`] = [acc[`${prefix}${filter.key}`], filter.value]
46+
}
47+
} else {
48+
acc[`${prefix}${filter.key}`] = filter.value
49+
}
50+
return acc
51+
}, {})
52+
53+
export const convertAppStateToUrlState = (appState: any) => {
54+
const activeFiltersForUrl = getFiltersForUrl("f_", appState.activeFilters || {})
55+
56+
return {
57+
...activeFiltersForUrl,
58+
searchTerm: appState.searchTerm || undefined,
59+
violationGroup: appState.violationGroup || undefined,
60+
}
61+
}
62+
63+
export const getFiltersForApp = (prefix: string, urlState: Record<string, string | string[]>) => {
64+
return Object.entries(urlState)
65+
.filter(([key]) => key.startsWith(prefix))
66+
.reduce((acc: Array<{ key: string; value: string }>, [key, value]) => {
67+
const filterKey = key.replace(prefix, "")
68+
if (value === undefined || value === null) return acc
69+
// if the value is an array, add each value as a separate filter
70+
if (Array.isArray(value)) {
71+
acc = [
72+
...acc,
73+
...value.map((v) => ({
74+
key: filterKey,
75+
value: v.trim(),
76+
})),
77+
]
78+
}
79+
// if the value is a string, add it as a single filter
80+
else if (typeof value === "string") {
81+
acc.push({ key: filterKey, value: value.trim() })
82+
}
83+
return acc
84+
}, [])
85+
}
86+
87+
export const convertUrlStateToAppState = (urlState: any) => {
88+
return {
89+
activeFilters: getFiltersForApp("f_", urlState),
90+
searchTerm: urlState.searchTerm,
91+
violationGroup: urlState.violationGroup,
92+
}
93+
}
94+
95+
// removes a specific filter from the filters object and returns a new object
96+
export const removeFilter = (filters: any, filterKey: string, filterValue: string) => {
97+
// if filter has more than one value, remove the specific value
98+
if (Array.isArray(filters?.[filterKey])) {
99+
const updatedFilters: any = {
100+
...filters,
101+
[filterKey]: filters?.[filterKey].filter((value: string) => value !== filterValue),
102+
}
103+
if (updatedFilters?.[filterKey]?.length === 0) {
104+
delete updatedFilters[filterKey]
105+
}
106+
return updatedFilters
107+
}
108+
109+
// if filter is a single value, remove the filter key
110+
const updatedFilters = { ...filters }
111+
if (updatedFilters?.[filterKey] === filterValue) delete updatedFilters[filterKey]
112+
113+
return updatedFilters
114+
}
115+
116+
// adds a specific filter to the filters object and returns a new object
117+
export const addFilter = (filters: any, filterKey: string, filterValue: string) => {
118+
// if the filter already exists, add the value to the existing array
119+
if (filters?.[filterKey] && Array.isArray(filters[filterKey])) {
120+
// Create a Set from existing values to remove duplicates
121+
const filterSet = new Set(filters[filterKey])
122+
// Add the new value
123+
filterSet.add(filterValue)
124+
// Convert back to array
125+
return {
126+
...filters,
127+
[filterKey]: Array.from(filterSet),
128+
}
129+
}
130+
// If the filter is a single value, convert it to an array
131+
if (filters?.[filterKey] && typeof filters[filterKey] === "string") {
132+
return {
133+
...filters,
134+
[filterKey]: [filters[filterKey], filterValue],
135+
}
136+
}
137+
// If the filter does not exist, create a new key with the value
138+
return {
139+
...filters,
140+
[filterKey]: filterValue,
141+
}
142+
}

apps/doop/src/routeTree.gen.ts

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,43 +8,60 @@
88
// You should NOT make any changes in this file as it will be overwritten.
99
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
1010

11-
import { Route as rootRouteImport } from "./routes/__root"
12-
import { Route as IndexRouteImport } from "./routes/index"
11+
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as ViolationsRouteImport } from './routes/violations'
13+
import { Route as IndexRouteImport } from './routes/index'
1314

15+
const ViolationsRoute = ViolationsRouteImport.update({
16+
id: '/violations',
17+
path: '/violations',
18+
getParentRoute: () => rootRouteImport,
19+
} as any)
1420
const IndexRoute = IndexRouteImport.update({
15-
id: "/",
16-
path: "/",
21+
id: '/',
22+
path: '/',
1723
getParentRoute: () => rootRouteImport,
1824
} as any)
1925

2026
export interface FileRoutesByFullPath {
21-
"/": typeof IndexRoute
27+
'/': typeof IndexRoute
28+
'/violations': typeof ViolationsRoute
2229
}
2330
export interface FileRoutesByTo {
24-
"/": typeof IndexRoute
31+
'/': typeof IndexRoute
32+
'/violations': typeof ViolationsRoute
2533
}
2634
export interface FileRoutesById {
2735
__root__: typeof rootRouteImport
28-
"/": typeof IndexRoute
36+
'/': typeof IndexRoute
37+
'/violations': typeof ViolationsRoute
2938
}
3039
export interface FileRouteTypes {
3140
fileRoutesByFullPath: FileRoutesByFullPath
32-
fullPaths: "/"
41+
fullPaths: '/' | '/violations'
3342
fileRoutesByTo: FileRoutesByTo
34-
to: "/"
35-
id: "__root__" | "/"
43+
to: '/' | '/violations'
44+
id: '__root__' | '/' | '/violations'
3645
fileRoutesById: FileRoutesById
3746
}
3847
export interface RootRouteChildren {
3948
IndexRoute: typeof IndexRoute
49+
ViolationsRoute: typeof ViolationsRoute
4050
}
4151

42-
declare module "@tanstack/react-router" {
52+
declare module '@tanstack/react-router' {
4353
interface FileRoutesByPath {
44-
"/": {
45-
id: "/"
46-
path: "/"
47-
fullPath: "/"
54+
'/violations': {
55+
id: '/violations'
56+
path: '/violations'
57+
fullPath: '/violations'
58+
preLoaderRoute: typeof ViolationsRouteImport
59+
parentRoute: typeof rootRouteImport
60+
}
61+
'/': {
62+
id: '/'
63+
path: '/'
64+
fullPath: '/'
4865
preLoaderRoute: typeof IndexRouteImport
4966
parentRoute: typeof rootRouteImport
5067
}
@@ -53,5 +70,8 @@ declare module "@tanstack/react-router" {
5370

5471
const rootRouteChildren: RootRouteChildren = {
5572
IndexRoute: IndexRoute,
73+
ViolationsRoute: ViolationsRoute,
5674
}
57-
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>()
75+
export const routeTree = rootRouteImport
76+
._addFileChildren(rootRouteChildren)
77+
._addFileTypes<FileRouteTypes>()

apps/doop/src/routes/index.tsx

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,8 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import React from "react"
7-
import { createFileRoute } from "@tanstack/react-router"
8-
import AppContent from "../components/AppContent"
6+
import { createFileRoute, redirect } from "@tanstack/react-router"
97

108
export const Route = createFileRoute("/")({
11-
component: RouteComponent,
9+
loader: () => redirect({ to: "/violations", search: (prev) => ({ ...prev }) }), // redirect to the default /violations page
1210
})
13-
14-
function RouteComponent() {
15-
const { appProps } = Route.useRouteContext()
16-
17-
return (
18-
<>
19-
<AppContent id={appProps?.id} showDebugSeverities={appProps.showDebugSeverities} />
20-
</>
21-
)
22-
}

0 commit comments

Comments
 (0)