diff --git a/.changeset/stupid-doors-rescue.md b/.changeset/stupid-doors-rescue.md
new file mode 100644
index 0000000000..4823846b6b
--- /dev/null
+++ b/.changeset/stupid-doors-rescue.md
@@ -0,0 +1,6 @@
+---
+"@cloudoperators/juno-app-greenhouse": patch
+"@cloudoperators/juno-app-heureka": patch
+---
+
+Implements initial filter URL synchronization with removable filter pills and adds context store to prevent re-application during tab navigation.
diff --git a/apps/heureka/src/App.tsx b/apps/heureka/src/App.tsx
index 77ad832286..0093a126a5 100644
--- a/apps/heureka/src/App.tsx
+++ b/apps/heureka/src/App.tsx
@@ -13,6 +13,7 @@ import styles from "./styles.css?inline"
import { ErrorBoundary } from "./components/common/ErrorBoundary"
import { getClient } from "./apollo-client"
import { routeTree } from "./routeTree.gen"
+import { StoreProvider } from "./store/StoreProvider"
export type InitialFilters = {
support_group?: string[]
@@ -91,7 +92,9 @@ const App = (props: AppProps) => {
-
+
+
+
diff --git a/apps/heureka/src/components/Services/Services.test.tsx b/apps/heureka/src/components/Services/Services.test.tsx
index aea49fd9df..5a5cdcdf9f 100644
--- a/apps/heureka/src/components/Services/Services.test.tsx
+++ b/apps/heureka/src/components/Services/Services.test.tsx
@@ -11,6 +11,7 @@ import { Services } from "./index"
import { Filter, FilterSettings } from "../common/Filters/types"
import { getTestRouter } from "../../mocks/getTestRouter"
import { mockServicesPromise } from "../../mocks/promises"
+import { StoreProvider } from "../../store/StoreProvider"
const mockFilters: Filter[] = [
{
@@ -44,7 +45,9 @@ const renderComponent = () => {
}),
component: () => (
-
+
+
+
),
})
diff --git a/apps/heureka/src/components/Services/ServicesFilters.tsx b/apps/heureka/src/components/Services/ServicesFilters.tsx
index bc8ea51dbd..77cb038a72 100644
--- a/apps/heureka/src/components/Services/ServicesFilters.tsx
+++ b/apps/heureka/src/components/Services/ServicesFilters.tsx
@@ -8,6 +8,7 @@ import { useLoaderData, useNavigate } from "@tanstack/react-router"
import { Filters } from "../common/Filters"
import { FilterSettings } from "../common/Filters/types"
import { getFiltersForUrl } from "./utils"
+import { SELECTED_FILTER_PREFIX } from "../../constants"
export const ServicesFilters = () => {
const navigate = useNavigate()
@@ -17,12 +18,19 @@ export const ServicesFilters = () => {
(updatedFilterSettings: FilterSettings) => {
navigate({
to: "/services",
- search: {
- ...getFiltersForUrl(updatedFilterSettings),
+ search: (prev) => {
+ const newFilterParams = getFiltersForUrl(updatedFilterSettings)
+ const cleanedPrev = Object.fromEntries(
+ Object.entries(prev).filter(([key]) => !key.startsWith(SELECTED_FILTER_PREFIX))
+ )
+ return {
+ ...cleanedPrev,
+ ...newFilterParams,
+ }
},
})
},
- [filterSettings, navigate]
+ [navigate]
)
return (
diff --git a/apps/heureka/src/components/Services/utils.ts b/apps/heureka/src/components/Services/utils.ts
index a6cdf2bb5c..a93489f83d 100644
--- a/apps/heureka/src/components/Services/utils.ts
+++ b/apps/heureka/src/components/Services/utils.ts
@@ -279,22 +279,22 @@ export const getNormalizedImageVersionIssuesResponse = (data: any): NormalizedIm
* }
*/
export const getFiltersForUrl = (filterSettings: FilterSettings): Record => {
- if (!filterSettings?.selectedFilters) {
- return {}
+ const result: Record = {
+ searchTerm: filterSettings.searchTerm || "",
}
- return {
- searchTerm: filterSettings.searchTerm || "",
- ...filterSettings.selectedFilters.reduce>((acc, filter) => {
+ if (filterSettings?.selectedFilters && filterSettings.selectedFilters.length > 0) {
+ filterSettings.selectedFilters.forEach((filter) => {
const key = `${SELECTED_FILTER_PREFIX}${filter.name}`
- if (acc[key]) {
- acc[key] = Array.isArray(acc[key]) ? [...acc[key], filter.value] : [acc[key], filter.value]
+ if (result[key]) {
+ result[key] = Array.isArray(result[key]) ? [...result[key], filter.value] : [result[key], filter.value]
} else {
- acc[key] = filter.value
+ result[key] = filter.value
}
- return acc
- }, {}),
+ })
}
+
+ return result
}
export const getNormalizedFilters = (data: GetServiceFiltersQuery | undefined | null): Filter[] =>
diff --git a/apps/heureka/src/components/Vulnerabilities/VulnerabilitiesFilters.tsx b/apps/heureka/src/components/Vulnerabilities/VulnerabilitiesFilters.tsx
index 999cff6461..2359490888 100644
--- a/apps/heureka/src/components/Vulnerabilities/VulnerabilitiesFilters.tsx
+++ b/apps/heureka/src/components/Vulnerabilities/VulnerabilitiesFilters.tsx
@@ -8,6 +8,7 @@ import { useLoaderData, useNavigate } from "@tanstack/react-router"
import { Filters } from "../common/Filters"
import { FilterSettings } from "../common/Filters/types"
import { getFiltersForUrl } from "./utils"
+import { SELECTED_FILTER_PREFIX } from "../../constants"
export const VulnerabilitiesFilters = () => {
const navigate = useNavigate()
@@ -17,12 +18,24 @@ export const VulnerabilitiesFilters = () => {
(updatedFilterSettings: FilterSettings) => {
navigate({
to: "/vulnerabilities",
- search: {
- ...getFiltersForUrl(updatedFilterSettings),
+ search: (prev) => {
+ // Get the new filter URL params
+ const newFilterParams = getFiltersForUrl(updatedFilterSettings)
+
+ // Remove all existing filter params from prev
+ const cleanedPrev = Object.fromEntries(
+ Object.entries(prev).filter(([key]) => !key.startsWith(SELECTED_FILTER_PREFIX))
+ )
+
+ // Merge with new filter params
+ return {
+ ...cleanedPrev,
+ ...newFilterParams,
+ }
},
})
},
- [filterSettings, navigate]
+ [navigate]
)
return (
diff --git a/apps/heureka/src/components/common/Filters/SelectedFilters.tsx b/apps/heureka/src/components/common/Filters/SelectedFilters.tsx
index 6d66c1d72b..aea1267888 100644
--- a/apps/heureka/src/components/common/Filters/SelectedFilters.tsx
+++ b/apps/heureka/src/components/common/Filters/SelectedFilters.tsx
@@ -16,7 +16,7 @@ export const SelectedFilters = ({ selectedFilters, onDelete }: SelectedFiltersPr
{selectedFilters?.map((filter) => (
{
+ beforeLoad: ({ search }) => {
const filterSettings = extractFilterSettingsFromSearchParams(search)
return {
- filterSettings:
- // Filters from the URL always have preference over initial filters
- (filterSettings?.selectedFilters ?? []).length > 0
- ? filterSettings
- : {
- ...filterSettings,
- selectedFilters: getInitialFilters(appProps?.initialFilters),
- },
+ filterSettings,
}
},
loader: async ({ context }) => {
@@ -79,5 +74,38 @@ export const Route = createFileRoute("/services/")({
filterSettings: sanitizeFilterSettings(filters, filterSettings), // we need to only apply filters that backend supports hence this sanitization
}
},
- component: Services,
+ component: RouteComponent,
})
+
+function RouteComponent() {
+ const navigate = useNavigate()
+ const { appProps } = useRouteContext({ from: "/services/" })
+ const search = useSearch({ from: "/services/" })
+ const { hasAppliedInitialFilters, markInitialFiltersApplied } = useStore()
+
+ // Use store to track initial filters across tab navigation - prevents re-application when switching between services/vulnerabilities tabs
+ useLayoutEffect(() => {
+ if (hasAppliedInitialFilters) return
+
+ // Use parsed search params from TanStack Router
+ const hasUrlFilters = Object.keys(search).some((key) => key.startsWith(SELECTED_FILTER_PREFIX))
+
+ if (!hasUrlFilters && appProps?.initialFilters?.support_group?.length) {
+ const initialFilters = getInitialFilters(appProps.initialFilters)
+
+ if (initialFilters.length > 0) {
+ navigate({
+ to: "/services",
+ search: getFiltersForUrl({
+ searchTerm: "",
+ selectedFilters: initialFilters,
+ }),
+ replace: true,
+ })
+ markInitialFiltersApplied()
+ }
+ }
+ }, [navigate, appProps, hasAppliedInitialFilters, markInitialFiltersApplied, search])
+
+ return
+}
diff --git a/apps/heureka/src/store/StoreProvider.tsx b/apps/heureka/src/store/StoreProvider.tsx
new file mode 100644
index 0000000000..4d3cbb1462
--- /dev/null
+++ b/apps/heureka/src/store/StoreProvider.tsx
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Juno contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { createContext, useContext, useState, ReactNode } from "react"
+
+// Store state interface - easily extensible for future global state
+interface StoreState {
+ hasAppliedInitialFilters: boolean
+}
+
+// Store actions interface - easily extensible for future actions
+interface StoreActions {
+ markInitialFiltersApplied: () => void
+}
+
+// Combined store context type
+interface StoreContextType extends StoreState, StoreActions {}
+
+const StoreContext = createContext(undefined)
+
+interface StoreProviderProps {
+ children: ReactNode
+}
+
+export const StoreProvider = ({ children }: StoreProviderProps) => {
+ // State management
+ const [hasAppliedInitialFilters, setHasAppliedInitialFilters] = useState(false)
+
+ // Actions
+ const markInitialFiltersApplied = () => {
+ setHasAppliedInitialFilters(true)
+ }
+ // Future actions can be added here
+
+ const storeValue: StoreContextType = {
+ // State
+ hasAppliedInitialFilters,
+
+ // Actions
+ markInitialFiltersApplied,
+ }
+
+ return {children}
+}
+
+export const useStore = () => {
+ const context = useContext(StoreContext)
+ if (context === undefined) {
+ throw new Error("useStore must be used within a StoreProvider")
+ }
+ return context
+}