mirror of
https://github.com/outbackdingo/openapi-ui.git
synced 2026-01-28 02:19:48 +00:00
instances map options pattern
This commit is contained in:
1
src/hooks/useNavSelector/index.ts
Normal file
1
src/hooks/useNavSelector/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './useNavSelector'
|
||||
@@ -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 }
|
||||
168
src/hooks/useNavSelector/utils.ts
Normal file
168
src/hooks/useNavSelector/utils.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user