Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { FallbackProps } from "react-error-boundary"
import { Message } from "@cloudoperators/juno-ui-components"

const ErrorFallback = ({ error }: FallbackProps) => (
<Message text={(error as Error)?.message || "An error occurred"} variant="danger" />
<Message text={error instanceof Error && error.message ? error.message : "An error occurred"} variant="danger" />
)

export default ErrorFallback
27 changes: 27 additions & 0 deletions apps/doop/src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,30 @@ export function capitalize(string: any) {

export const isObjectWithKeys = (value: any) =>
value !== null && typeof value === "object" && Object.keys(value).length > 0

/**
* Filters raw URL search params to only known keys and keys that start with any allowed prefix.
* Use this before Zod parsing when you need prefix-based catchall validation
* (Zod v4 no longer provides ctx.path in preprocess).
*
* Reusable pattern for route validateSearch: filter first, then parse with a schema that has
* .catchall(z.union([z.string(), z.array(z.string()), z.undefined()])).
*/
export function filterSearchParamsByPrefix(
raw: Record<string, unknown>,
knownKeys: string[],
allowedPrefixes: string[]
): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const key of knownKeys) {
if (Object.prototype.hasOwnProperty.call(raw, key)) {
result[key] = raw[key]
}
}
for (const key of Object.keys(raw)) {
if (allowedPrefixes.some((p) => key.startsWith(p))) {
result[key] = raw[key]
}
}
return result
}
24 changes: 12 additions & 12 deletions apps/doop/src/routes/violations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,26 @@ import {
} from "../components/StoreProvider"
import { parseInitialFilters } from "../lib/store/createFiltersSlice"
import { isObjectWithKeys } from "../lib/helpers"
import { filterSearchParamsByPrefix } from "../lib/helpers"

const filterValueSchema = z.union([z.string(), z.array(z.string()), z.undefined()])

const searchSchema = z
.object({
searchTerm: z.string().optional(),
violationGroup: z.string().optional(),
})
.catchall(
z.preprocess(
(val, ctx) => {
if (ctx.path.length > 0 && typeof ctx.path[0] === "string" && !ctx.path[0].startsWith(ACTIVE_FILTERS_PREFIX)) {
return undefined
}
return val
},
z.union([z.string(), z.array(z.string()), z.undefined()])
)
)
.catchall(filterValueSchema)

const VIOLATIONS_KNOWN_KEYS = ["searchTerm", "violationGroup"] as const

function validateViolationsSearch(search: Record<string, unknown>): z.infer<typeof searchSchema> {
const filtered = filterSearchParamsByPrefix(search, [...VIOLATIONS_KNOWN_KEYS], [ACTIVE_FILTERS_PREFIX])
return searchSchema.parse(filtered)
}

export const Route = createFileRoute("/violations")({
validateSearch: searchSchema,
validateSearch: validateViolationsSearch,
beforeLoad: ({ search }) => {
// extract alerts specific state from the URL search params
const { activeFilters, searchTerm, violationGroup } = convertUrlStateToAppState(search)
Expand Down
27 changes: 27 additions & 0 deletions apps/greenhouse/src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,30 @@ export const parseError = (error: any) => {
// @ts-expect-error TS(2304): Cannot find name 'errMsg'.
return errMsg
}

/**
* Filters raw URL search params to only known keys and keys that start with any allowed prefix.
* Use this before Zod parsing when you need prefix-based catchall validation
* (Zod v4 no longer provides ctx.path in preprocess).
*
* Reusable pattern for route validateSearch: filter first, then parse with a schema that has
* .catchall(z.union([z.string(), z.array(z.string()), z.undefined()])).
*/
export function filterSearchParamsByPrefix(
raw: Record<string, unknown>,
knownKeys: string[],
allowedPrefixes: string[]
): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const key of knownKeys) {
if (Object.prototype.hasOwnProperty.call(raw, key)) {
result[key] = raw[key]
}
}
for (const key of Object.keys(raw)) {
if (allowedPrefixes.some((p) => key.startsWith(p))) {
result[key] = raw[key]
}
}
return result
}
24 changes: 12 additions & 12 deletions apps/greenhouse/src/routes/admin/plugin-presets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,28 @@ import { z } from "zod"
import { PluginPresets } from "../../components/admin/PluginPresets"
import { SELECTED_FILTER_PREFIX } from "../../components/admin/constants"
import { extractFilterSettingsFromSearchParams } from "../../components/admin/utils"
import { filterSearchParamsByPrefix } from "../../lib/helpers"

const filterValueSchema = z.union([z.string(), z.array(z.string()), z.undefined()])

const searchParamsSchema = z
.object({
searchTerm: z.string().optional(),
})
.catchall(
z.preprocess(
(val, ctx) => {
if (ctx.path.length > 0 && typeof ctx.path[0] === "string" && !ctx.path[0].startsWith(SELECTED_FILTER_PREFIX)) {
return undefined
}
return val
},
z.union([z.string(), z.array(z.string()), z.undefined()])
)
)
.catchall(filterValueSchema)

export type PluginPresetSearchParams = z.infer<typeof searchParamsSchema>

const PLUGIN_PRESETS_KNOWN_KEYS = ["searchTerm"] as const

function validatePluginPresetsSearch(search: Record<string, unknown>): PluginPresetSearchParams {
const filtered = filterSearchParamsByPrefix(search, [...PLUGIN_PRESETS_KNOWN_KEYS], [SELECTED_FILTER_PREFIX])
return searchParamsSchema.parse(filtered)
}

export const Route = createFileRoute("/admin/plugin-presets")({
component: PluginPresets,
validateSearch: (search: Record<string, unknown>) => searchParamsSchema.parse(search),
validateSearch: validatePluginPresetsSearch,
loaderDeps: (search) => ({
...search,
}),
Expand Down
25 changes: 12 additions & 13 deletions apps/heureka/src/routes/services/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,28 @@ import { SELECTED_FILTER_PREFIX } from "../../constants"
import { fetchServices } from "../../api/fetchServices"
import { fetchServicesFilters } from "../../api/fetchServicesFilters"
import { extractFilterSettingsFromSearchParams } from "../../components/Services/utils"
import { filterSearchParamsByPrefix } from "../../utils"

const filterValueSchema = z.union([z.string(), z.array(z.string()), z.undefined()])

// Schema for validating and transforming search parameters related to /services page.
const servicesSearchSchema = z
.object({
service: z.string().optional(),
searchTerm: z.string().optional(),
})
.catchall(
z.preprocess(
(val, ctx) => {
if (ctx.path.length > 0 && typeof ctx.path[0] === "string" && !ctx.path[0].startsWith(SELECTED_FILTER_PREFIX)) {
return undefined
}
return val
},
z.union([z.string(), z.array(z.string()), z.undefined()])
)
)
.catchall(filterValueSchema)

export type ServicesSearchParams = z.infer<typeof servicesSearchSchema>

const SERVICES_KNOWN_KEYS = ["service", "searchTerm"] as const

function validateServicesSearch(search: Record<string, unknown>): ServicesSearchParams {
const filtered = filterSearchParamsByPrefix(search, [...SERVICES_KNOWN_KEYS], [SELECTED_FILTER_PREFIX])
return servicesSearchSchema.parse(filtered) as ServicesSearchParams
}

export const Route = createFileRoute("/services/")({
validateSearch: servicesSearchSchema,
validateSearch: validateServicesSearch,
loaderDeps: ({ search }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { service, ...rest } = search // we're omitting 'service' from the deps so route does not reload when it changes
Expand Down
24 changes: 12 additions & 12 deletions apps/heureka/src/routes/vulnerabilities/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,28 @@ import { SELECTED_FILTER_PREFIX } from "../../constants"
import { fetchVulnerabilities } from "../../api/fetchVulnerabilities"
import { fetchVulnerabilityFilters } from "../../api/fetchVulnerabilityFilters"
import { extractFilterSettingsFromSearchParams } from "../../components/Vulnerabilities/utils"
import { filterSearchParamsByPrefix } from "../../utils"

const filterValueSchema = z.union([z.string(), z.array(z.string()), z.undefined()])

const vulnerabilitiesSearchSchema = z
.object({
vulnerability: z.string().optional(),
searchTerm: z.preprocess((val) => (typeof val === "number" ? val.toString() : val), z.string().optional()),
})
.catchall(
z.preprocess(
(val, ctx) => {
if (ctx.path.length > 0 && typeof ctx.path[0] === "string" && !ctx.path[0].startsWith(SELECTED_FILTER_PREFIX)) {
return undefined
}
return val
},
z.union([z.string(), z.array(z.string()), z.undefined()])
)
)
.catchall(filterValueSchema)

export type VulnerabilitiesSearchParams = z.infer<typeof vulnerabilitiesSearchSchema>

const VULNERABILITIES_KNOWN_KEYS = ["vulnerability", "searchTerm"] as const

function validateVulnerabilitiesSearch(search: Record<string, unknown>): VulnerabilitiesSearchParams {
const filtered = filterSearchParamsByPrefix(search, [...VULNERABILITIES_KNOWN_KEYS], [SELECTED_FILTER_PREFIX])
return vulnerabilitiesSearchSchema.parse(filtered) as VulnerabilitiesSearchParams
}

export const Route = createFileRoute("/vulnerabilities/")({
validateSearch: vulnerabilitiesSearchSchema,
validateSearch: validateVulnerabilitiesSearch,
loaderDeps: ({ search }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { vulnerability, ...rest } = search // we're omitting 'service' from the deps so route does not reload when it changes
Expand Down
27 changes: 27 additions & 0 deletions apps/heureka/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,30 @@ export const mapObject = <T, U>(obj: Record<string, T>, iteratee: (value: T, key
})
return result
}

/**
* Filters raw URL search params to only known keys and keys that start with any allowed prefix.
* Use this before Zod parsing when you need prefix-based catchall validation
* (Zod v4 no longer provides ctx.path in preprocess).
*
* Reusable pattern for route validateSearch: filter first, then parse with a schema that has
* .catchall(z.union([z.string(), z.array(z.string()), z.undefined()])).
*/
export function filterSearchParamsByPrefix(
raw: Record<string, unknown>,
knownKeys: string[],
allowedPrefixes: string[]
): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const key of knownKeys) {
if (Object.prototype.hasOwnProperty.call(raw, key)) {
result[key] = raw[key]
}
}
for (const key of Object.keys(raw)) {
if (allowedPrefixes.some((p) => key.startsWith(p))) {
result[key] = raw[key]
}
}
return result
}
19 changes: 7 additions & 12 deletions apps/supernova/src/lib/urlStateUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,13 @@ export const extractSearchStringFromHashFragment = (searchString: string) => {
return searchString.slice(postHashParams, preHashParams === -1 ? undefined : preHashParams)
}

export const getFiltersForUrl = (prefix: string, filters: any) => {
return {
...Object.entries(filters)
.map(([key, value]) => {
if (value === undefined || value === null) return {}
if (Array.isArray(value)) {
return { [`${prefix}${key}`]: value }
}
return { [`${prefix}${key}`]: value }
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
}
export const getFiltersForUrl = (prefix: string, filters: any): Record<string, string | string[]> => {
return Object.entries(filters ?? {})
.filter(([, value]) => value !== undefined && value !== null)
.reduce<Record<string, string | string[]>>((acc, [key, value]) => {
acc[`${prefix}${key}`] = Array.isArray(value) ? value : (value as string)
return acc
}, {})
}

export const convertAppStateToUrlState = (appState: any) => {
Expand Down
27 changes: 27 additions & 0 deletions apps/supernova/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,30 @@ export const sortAlerts = (items: any) => {

export const isObjectWithKeys = (value: any) =>
value !== null && typeof value === "object" && Object.keys(value).length > 0

/**
* Filters raw URL search params to only known keys and keys that start with any allowed prefix.
* Use this before Zod parsing when you need prefix-based catchall validation
* (Zod v4 no longer provides ctx.path in preprocess).
*
* Reusable pattern for route validateSearch: filter first, then parse with a schema that has
* .catchall(z.union([z.string(), z.array(z.string()), z.undefined()])).
*/
export function filterSearchParamsByPrefix(
raw: Record<string, unknown>,
knownKeys: string[],
allowedPrefixes: string[]
): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const key of knownKeys) {
if (Object.prototype.hasOwnProperty.call(raw, key)) {
result[key] = raw[key]
}
}
for (const key of Object.keys(raw)) {
if (allowedPrefixes.some((p) => key.startsWith(p))) {
result[key] = raw[key]
}
}
return result
}
31 changes: 15 additions & 16 deletions apps/supernova/src/routes/alerts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,31 @@ import { ACTIVE_FILTERS_PREFIX, PAUSED_FILTERS_PREFIX } from "../constants"
import { convertUrlStateToAppState, getFiltersForUrl } from "../lib/urlStateUtils"
import { useGlobalsActions, useFilterActions, useGlobalsInitialFiltersApplied } from "../components/StoreProvider"
import { isObjectWithKeys } from "../lib/utils"
import { filterSearchParamsByPrefix } from "../lib/utils"

const filterValueSchema = z.union([z.string(), z.array(z.string()), z.undefined()])

const searchSchema = z
.object({
searchTerm: z.string().optional(),
showDetailsFor: z.string().optional(),
predefinedFilter: z.string().optional(),
})
.catchall(
z.preprocess(
(val, ctx) => {
if (
ctx.path.length > 0 &&
typeof ctx.path[0] === "string" &&
!ctx.path[0].startsWith(ACTIVE_FILTERS_PREFIX) &&
!ctx.path[0].startsWith(PAUSED_FILTERS_PREFIX)
) {
return undefined
}
return val
},
z.union([z.string(), z.array(z.string()), z.undefined()])
)
.catchall(filterValueSchema)

const ALERTS_KNOWN_KEYS = ["searchTerm", "showDetailsFor", "predefinedFilter"] as const

function validateAlertsSearch(search: Record<string, unknown>): z.infer<typeof searchSchema> {
const filtered = filterSearchParamsByPrefix(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above let's try to remove two sources of truths for known keys.

search,
[...ALERTS_KNOWN_KEYS],
[ACTIVE_FILTERS_PREFIX, PAUSED_FILTERS_PREFIX]
)
return searchSchema.parse(filtered)
}

export const Route = createFileRoute("/alerts")({
validateSearch: searchSchema,
validateSearch: validateAlertsSearch,
beforeLoad: ({ search }) => {
// extract alerts specific state from the URL search params
const { activeFilters, pausedFilters, predefinedFilter, searchTerm, showDetailsFor } =
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,6 @@
"packageManager": "[email protected]",
"dependencies": {
"@tanstack/react-query": "5.89.0",
"zod": "3.25.76"
"zod": "4.3.6"
}
}
Loading
Loading