diff --git a/src/hooks/useNavSelector/index.ts b/src/hooks/useNavSelector/index.ts new file mode 100644 index 0000000..c27b890 --- /dev/null +++ b/src/hooks/useNavSelector/index.ts @@ -0,0 +1 @@ +export * from './useNavSelector' diff --git a/src/hooks/useNavSelector.ts b/src/hooks/useNavSelector/useNavSelector.ts similarity index 53% rename from src/hooks/useNavSelector.ts rename to src/hooks/useNavSelector/useNavSelector.ts index 2d3bed0..6acb35d 100644 --- a/src/hooks/useNavSelector.ts +++ b/src/hooks/useNavSelector/useNavSelector.ts @@ -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 } diff --git a/src/hooks/useNavSelector/utils.ts b/src/hooks/useNavSelector/utils.ts new file mode 100644 index 0000000..91bfb7f --- /dev/null +++ b/src/hooks/useNavSelector/utils.ts @@ -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 => { + 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).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[]['']} + // const placeholderRegex = /\{reqsJsonPath\[(\d+)\]\s*\[\s*(['"])([^'"]+)\2\s*\]\}/g + + // Regex to match either: + // 1) {reqsJsonPath[]['']} + // 2) {reqsJsonPath[]['']['']} + // 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 + multiQueryData: TDataMap +}): string => { + return parsePartsOfUrl({ + template: parseJsonPathTemplate({ + text: parseMutliqueryText({ + text, + multiQueryData, + }), + multiQueryData, + }), + replaceValues, + }) +}