instances map options pattern

This commit is contained in:
typescreep
2025-09-16 15:46:50 +03:00
parent 265c256e05
commit f0ea0473a7
3 changed files with 212 additions and 5 deletions

View File

@@ -0,0 +1 @@
export * from './useNavSelector'

View File

@@ -1,7 +1,16 @@
import { useApiResources, TClusterList, TSingleResource } from '@prorobotech/openapi-k8s-toolkit'
import {
useApiResources,
TClusterList,
TSingleResource,
useDirectUnknownResource,
} from '@prorobotech/openapi-k8s-toolkit'
import { useSelector } from 'react-redux'
import { RootState } from 'store/store'
import {
BASE_API_GROUP,
BASE_API_VERSION,
BASE_CUSTOMIZATION_NAVIGATION_RESOURCE_NAME,
BASE_CUSTOMIZATION_NAVIGATION_RESOURCE,
BASE_PROJECTS_API_GROUP,
BASE_PROJECTS_VERSION,
BASE_PROJECTS_RESOURCE_NAME,
@@ -9,6 +18,7 @@ import {
BASE_INSTANCES_VERSION,
BASE_INSTANCES_RESOURCE_NAME,
} from 'constants/customizationApiGroupAndVersion'
import { parseAll } from './utils'
const mappedClusterToOptionInSidebar = ({ name }: TClusterList[number]): { value: string; label: string } => ({
value: name,
@@ -20,14 +30,35 @@ const mappedProjectToOptionInSidebar = ({ metadata }: TSingleResource): { value:
label: metadata.name,
})
const mappedInstanceToOptionInSidebar = ({ metadata }: TSingleResource): { value: string; label: string } => ({
value: `${metadata.namespace}-${metadata.name}`,
label: metadata.name,
const mappedInstanceToOptionInSidebar = ({
instance,
templateString,
}: {
instance: TSingleResource
templateString?: string
}): { value: string; label: string } => ({
value: templateString
? parseAll({
text: templateString,
replaceValues: {},
multiQueryData: { req0: { ...instance } },
})
: `${instance.metadata.namespace}-${instance.metadata.name}`,
label: instance.metadata.name,
})
export const useNavSelector = (clusterName?: string, projectName?: string) => {
const clusterList = useSelector((state: RootState) => state.clusterList.clusterList)
const { data: navigationData } = useDirectUnknownResource<{
spec: { instances: { mapOptionsPattern: string } }
}>({
uri: `/api/clusters/${clusterName}/k8s/apis/${BASE_API_GROUP}/${BASE_API_VERSION}/${BASE_CUSTOMIZATION_NAVIGATION_RESOURCE_NAME}/${BASE_CUSTOMIZATION_NAVIGATION_RESOURCE}`,
refetchInterval: false,
queryKey: ['navigation', clusterName || 'no-cluster'],
isEnabled: clusterName !== undefined,
})
const { data: projects } = useApiResources({
clusterName: clusterName || '',
namespace: '',
@@ -50,7 +81,14 @@ export const useNavSelector = (clusterName?: string, projectName?: string) => {
const projectsInSidebar = clusterName && projects ? projects.items.map(mappedProjectToOptionInSidebar) : []
const instancesInSidebar =
clusterName && instances
? instances.items.filter(item => item.metadata.namespace === projectName).map(mappedInstanceToOptionInSidebar)
? instances.items
.filter(item => item.metadata.namespace === projectName)
.map(item =>
mappedInstanceToOptionInSidebar({
instance: item,
templateString: navigationData?.spec.instances.mapOptionsPattern,
}),
)
: []
return { clustersInSidebar, projectsInSidebar, instancesInSidebar, allInstancesLoadingSuccess }

View File

@@ -0,0 +1,168 @@
import _ from 'lodash'
import jp from 'jsonpath'
import { prepareTemplate } from '@prorobotech/openapi-k8s-toolkit'
export const parsePartsOfUrl = ({
template,
replaceValues,
}: {
template: string
replaceValues: Record<string, string | undefined>
}): string => {
return prepareTemplate({ template, replaceValues })
}
type TDataMap = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
export const parseMutliqueryText = ({
text,
multiQueryData,
customFallback,
}: {
text?: string
multiQueryData: TDataMap
customFallback?: string
}): string => {
if (!text) {
return ''
}
// 1: req index
// 2: comma-separated quoted keys
// 3: optional quoted fallback
// return text.replace(/\{reqs\[(\d+)\]\[((?:\s*['"][^'"]+['"]\s*,?)+)\]\}/g, (match, reqIndexStr, rawPath) => {
return text.replace(
/\{reqs\[(\d+)\]\[((?:\s*['"][^'"]+['"]\s*,?)+)\](?:\[\s*['"]([^'"]+)['"]\s*\])?\}/g,
(_match, reqIndexStr, rawPath, fallback) => {
try {
const reqIndex = parseInt(reqIndexStr, 10)
// Extract quoted keys into a path array using another regex
// Matches: 'key', "another", 'deeply_nested'
// Explanation:
// ['"] - opening quote (single or double)
// ([^'"]+) - capture group: any characters that are not quotes
// ['"] - closing quote
const path = Array.from(rawPath.matchAll(/['"]([^'"]+)['"]/g) as IterableIterator<RegExpMatchArray>).map(
m => m[1],
)
// Use lodash.get to safely access deep value
const value = _.get(multiQueryData[`req${reqIndex}`], path, fallback !== undefined ? fallback : undefined)
if (value == null && !customFallback) {
return fallback ?? 'Undefined with no fallback'
}
if (customFallback && (value === undefined || value === null)) {
return customFallback
}
return String(value)
} catch {
return _match // fallback to original if anything fails
}
},
)
}
export const parseJsonPathTemplate = ({
text,
multiQueryData,
customFallback,
}: {
text?: string
multiQueryData: TDataMap
customFallback?: string
}): string => {
if (!text) return ''
// Regex to match: {reqsJsonPath[<index>]['<jsonpath>']}
// const placeholderRegex = /\{reqsJsonPath\[(\d+)\]\s*\[\s*(['"])([^'"]+)\2\s*\]\}/g
// Regex to match either:
// 1) {reqsJsonPath[<index>]['<path>']}
// 2) {reqsJsonPath[<index>]['<path>']['<fallback>']}
// const placeholderRegex = /\{reqsJsonPath\[(\d+)\]\s*\[\s*(['"])([^'"]+)\2\s*\](?:\s*\[\s*(['"])([^'"]*)\4\s*\])?\}/g
const placeholderRegex =
/\{reqsJsonPath\[(\d+)\]\s*\[\s*(['"])([\s\S]*?)\2\s*\](?:\s*\[\s*(['"])([\s\S]*?)\4\s*\])?\}/g
return text.replace(
placeholderRegex,
(match, reqIndexStr, _quote, jsonPathExpr, _smth, fallback = 'Undefined with no fallback') => {
try {
const reqIndex = parseInt(reqIndexStr, 10)
const jsonRoot = multiQueryData[`req${reqIndex}`]
if (jsonRoot === undefined && !customFallback) {
// return ''
// no such request entry → use fallback (or empty)
return fallback
}
if (jsonRoot === undefined && customFallback) {
return customFallback
}
// Evaluate JSONPath and pick first result
const results = jp.query(jsonRoot, `$${jsonPathExpr}`)
// if (results.length === 0) {
// return ''
// }
if (results.length === 0 || results[0] == null || results[0] === undefined) {
if (customFallback) {
return customFallback
}
// no result or null → fallback
return fallback
}
// Return first result as string
return String(results[0])
} catch {
// On any error, leave the placeholder as-is
return match
}
},
)
}
export const parseWithoutPartsOfUrl = ({
text,
multiQueryData,
customFallback,
}: {
text: string
multiQueryData: TDataMap
customFallback?: string
}): string => {
return parseJsonPathTemplate({
text: parseMutliqueryText({
text,
multiQueryData,
customFallback,
}),
multiQueryData,
customFallback,
})
}
export const parseAll = ({
text,
replaceValues,
multiQueryData,
}: {
text: string
replaceValues: Record<string, string | undefined>
multiQueryData: TDataMap
}): string => {
return parsePartsOfUrl({
template: parseJsonPathTemplate({
text: parseMutliqueryText({
text,
multiQueryData,
}),
multiQueryData,
}),
replaceValues,
})
}