[dashboard] Migrate patches to upstream project

[dashboard] Fix nested lists in addtiionalProperties

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei Kvapil
2025-11-01 22:29:49 +01:00
parent 2ac533f2f6
commit 723eefea66
8 changed files with 262 additions and 47 deletions

View File

@@ -1,15 +1,11 @@
# imported from https://github.com/cozystack/openapi-ui-k8s-bff
ARG NODE_VERSION=20.18.1
FROM node:${NODE_VERSION}-alpine AS builder
RUN apk add git
WORKDIR /src
ARG COMMIT_REF=88531ed6881b4ce4808e56c00905951d7ba8031c
ARG COMMIT_REF=92906a7f21050cfb8e352f98d36b209c57844f63
RUN wget -O- https://github.com/PRO-Robotech/openapi-ui-k8s-bff/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1
COPY patches /patches
RUN git apply /patches/*.diff
ENV PATH=/src/node_modules/.bin:$PATH
RUN npm install
RUN npm run build

View File

@@ -1,21 +0,0 @@
diff --git a/src/endpoints/forms/prepareFormProps/prepareFormProps.ts b/src/endpoints/forms/prepareFormProps/prepareFormProps.ts
index 7e437db..90c40f6 100644
--- a/src/endpoints/forms/prepareFormProps/prepareFormProps.ts
+++ b/src/endpoints/forms/prepareFormProps/prepareFormProps.ts
@@ -15,6 +15,7 @@ export const prepareFormProps: RequestHandler = async (req: TPrepareFormReq, res
const filteredHeaders = { ...req.headers }
delete filteredHeaders['host'] // Avoid passing internal host header
+ delete filteredHeaders['content-length'] // This header causes "stream has been aborted"
const { data: formsOverridesData } = await userKubeApi.get<TFormsOverridesData>(
`/apis/${BASE_API_GROUP}/${BASE_API_VERSION}/customformsoverrides`,
@@ -40,7 +41,7 @@ export const prepareFormProps: RequestHandler = async (req: TPrepareFormReq, res
},
)
- const { data: namespacesData } = await userKubeApi.get<TBuiltinResources>(`/api/v1/namespaces`, {
+ const { data: namespacesData } = await userKubeApi.get<TBuiltinResources>(`/apis/core.cozystack.io/v1alpha1/tenantnamespaces`, {
headers: {
// Authorization: `Bearer ${bearerToken}`,
// Cookie: cookies,

View File

@@ -5,7 +5,7 @@ ARG NODE_VERSION=20.18.1
FROM node:${NODE_VERSION}-alpine AS openapi-k8s-toolkit-builder
RUN apk add git
WORKDIR /src
ARG COMMIT=e5f16b45de19f892de269cc4ef27e74aa62f4c92
ARG COMMIT=7086a2d8a07dcf6a94bb4276433db5d84acfcf3b
RUN wget -O- https://github.com/cozystack/openapi-k8s-toolkit/archive/${COMMIT}.tar.gz | tar -xzvf- --strip-components=1
COPY openapi-k8s-toolkit/patches /patches
@@ -22,7 +22,7 @@ FROM node:${NODE_VERSION}-alpine AS builder
RUN apk add git
WORKDIR /src
ARG COMMIT_REF=9ce4367657f49c0032d8016b1d9491f8abbd2b15
ARG COMMIT_REF=fe237518348e94cead6d4f3283b2fce27f26aa12
RUN wget -O- https://github.com/PRO-Robotech/openapi-ui/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1
COPY openapi-ui/patches /patches

View File

@@ -0,0 +1,230 @@
diff --git a/src/components/molecules/BlackholeForm/molecules/FormObjectFromSwagger/FormObjectFromSwagger.tsx b/src/components/molecules/BlackholeForm/molecules/FormObjectFromSwagger/FormObjectFromSwagger.tsx
index a7135d4..2fea0bb 100644
--- a/src/components/molecules/BlackholeForm/molecules/FormObjectFromSwagger/FormObjectFromSwagger.tsx
+++ b/src/components/molecules/BlackholeForm/molecules/FormObjectFromSwagger/FormObjectFromSwagger.tsx
@@ -68,13 +68,60 @@ export const FormObjectFromSwagger: FC<TFormObjectFromSwaggerProps> = ({
properties?: OpenAPIV2.SchemaObject['properties']
required?: string
}
+
+ // Check if the field name exists in additionalProperties.properties
+ // If so, use the type from that property definition
+ const nestedProp = addProps?.properties?.[additionalPropValue] as OpenAPIV2.SchemaObject | undefined
+ let fieldType: string = addProps.type
+ let fieldItems: { type: string } | undefined = addProps.items
+ let fieldNestedProperties = addProps.properties || {}
+ let fieldRequired: string | undefined = addProps.required
+
+ if (nestedProp) {
+ // Use the nested property definition if it exists
+ // Handle type - it can be string or string[] in OpenAPI v2
+ if (nestedProp.type) {
+ if (Array.isArray(nestedProp.type)) {
+ fieldType = nestedProp.type[0] || addProps.type
+ } else if (typeof nestedProp.type === 'string') {
+ fieldType = nestedProp.type
+ } else {
+ fieldType = addProps.type
+ }
+ } else {
+ fieldType = addProps.type
+ }
+
+ // Handle items - it can be ItemsObject or ReferenceObject
+ if (nestedProp.items) {
+ // Check if it's a valid ItemsObject with type property
+ if ('type' in nestedProp.items && typeof nestedProp.items.type === 'string') {
+ fieldItems = { type: nestedProp.items.type }
+ } else {
+ fieldItems = addProps.items
+ }
+ } else {
+ fieldItems = addProps.items
+ }
+
+ fieldNestedProperties = nestedProp.properties || {}
+ // Handle required field - it can be string[] in OpenAPI schema
+ if (Array.isArray(nestedProp.required)) {
+ fieldRequired = nestedProp.required.join(',')
+ } else if (typeof nestedProp.required === 'string') {
+ fieldRequired = nestedProp.required
+ } else {
+ fieldRequired = addProps.required
+ }
+ }
+
inputProps?.addField({
path: Array.isArray(name) ? [...name, String(collapseTitle)] : [name, String(collapseTitle)],
name: additionalPropValue,
- type: addProps.type,
- items: addProps.items,
- nestedProperties: addProps.properties || {},
- required: addProps.required,
+ type: fieldType,
+ items: fieldItems,
+ nestedProperties: fieldNestedProperties,
+ required: fieldRequired,
})
setAddditionalPropValue(undefined)
}
diff --git a/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx b/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx
index 487d480..3ca46c1 100644
--- a/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx
+++ b/src/components/molecules/BlackholeForm/molecules/FormStringInput/FormStringInput.tsx
@@ -42,7 +42,11 @@ export const FormStringInput: FC<TFormStringInputProps> = ({
const formValue = Form.useWatch(formFieldName)
// Derive multiline based on current local value
- const isMultiline = useMemo(() => isMultilineString(formValue), [formValue])
+ const isMultiline = useMemo(() => {
+ // Normalize value for multiline check
+ const value = typeof formValue === 'string' ? formValue : (formValue === null || formValue === undefined ? '' : String(formValue))
+ return isMultilineString(value)
+ }, [formValue])
const title = (
<>
@@ -77,6 +81,23 @@ export const FormStringInput: FC<TFormStringInputProps> = ({
rules={[{ required: forceNonRequired === false && required?.includes(getStringByName(name)) }]}
validateTrigger="onBlur"
hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
+ normalize={(value) => {
+ // Normalize value to string - prevent "[object Object]" display
+ if (value === undefined || value === null) {
+ return ''
+ }
+ if (typeof value === 'string') {
+ return value
+ }
+ if (typeof value === 'number' || typeof value === 'boolean') {
+ return String(value)
+ }
+ // If it's an object or array, it shouldn't be in a string field - return empty string
+ if (typeof value === 'object') {
+ return ''
+ }
+ return String(value)
+ }}
>
<Input.TextArea
placeholder={getStringByName(name)}
diff --git a/src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/casts.ts b/src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/casts.ts
index 6f9eb39..835224c 100644
--- a/src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/casts.ts
+++ b/src/components/molecules/BlackholeForm/organisms/BlackholeForm/helpers/casts.ts
@@ -124,8 +124,26 @@ export const materializeAdditionalFromValues = (
*
* This is used when a new field appears in the data but doesn't yet exist in the schema.
*/
- const makeChildFromAP = (ap: any): OpenAPIV2.SchemaObject => {
- const t = ap?.type ?? 'object'
+ const makeChildFromAP = (ap: any, value?: unknown): OpenAPIV2.SchemaObject => {
+ // Determine type based on actual value if not explicitly defined in additionalProperties
+ let t = ap?.type
+ if (!t && value !== undefined && value !== null) {
+ if (Array.isArray(value)) {
+ t = 'array'
+ } else if (typeof value === 'object') {
+ t = 'object'
+ } else if (typeof value === 'string') {
+ t = 'string'
+ } else if (typeof value === 'number') {
+ t = 'number'
+ } else if (typeof value === 'boolean') {
+ t = 'boolean'
+ } else {
+ t = 'object'
+ }
+ }
+ t = t ?? 'object'
+
const child: OpenAPIV2.SchemaObject = { type: t } as any
// Copy common schema details (if present)
@@ -134,6 +152,20 @@ export const materializeAdditionalFromValues = (
if (ap?.required)
(child as any).required = _.cloneDeep(ap.required)
+ // If value is an array and items type is not defined, infer it from the first item
+ if (t === 'array' && Array.isArray(value) && value.length > 0 && !ap?.items) {
+ const firstItem = value[0]
+ if (typeof firstItem === 'string') {
+ ;(child as any).items = { type: 'string' }
+ } else if (typeof firstItem === 'number') {
+ ;(child as any).items = { type: 'number' }
+ } else if (typeof firstItem === 'boolean') {
+ ;(child as any).items = { type: 'boolean' }
+ } else if (typeof firstItem === 'object') {
+ ;(child as any).items = { type: 'object' }
+ }
+ }
+
// Mark as originating from `additionalProperties`
;(child as any).isAdditionalProperties = true
return child
@@ -177,7 +209,16 @@ export const materializeAdditionalFromValues = (
// If the key doesn't exist in schema, create it from `additionalProperties`
if (!schemaNode.properties![k]) {
- schemaNode.properties![k] = makeChildFromAP(ap)
+ // Check if there's a nested property definition in additionalProperties
+ const nestedProp = ap?.properties?.[k]
+ if (nestedProp) {
+ // Use the nested property definition from additionalProperties
+ schemaNode.properties![k] = _.cloneDeep(nestedProp) as any
+ ;(schemaNode.properties![k] as any).isAdditionalProperties = true
+ } else {
+ // Create from additionalProperties with value-based type inference
+ schemaNode.properties![k] = makeChildFromAP(ap, vo[k])
+ }
// If it's an existing additional property, merge any nested structure
} else if ((schemaNode.properties![k] as any).isAdditionalProperties && ap?.properties) {
;(schemaNode.properties![k] as any).properties ??= _.cloneDeep(ap.properties)
diff --git a/src/components/molecules/BlackholeForm/organisms/BlackholeForm/utils.tsx b/src/components/molecules/BlackholeForm/organisms/BlackholeForm/utils.tsx
index 2d887c7..d69d711 100644
--- a/src/components/molecules/BlackholeForm/organisms/BlackholeForm/utils.tsx
+++ b/src/components/molecules/BlackholeForm/organisms/BlackholeForm/utils.tsx
@@ -394,9 +394,11 @@ export const getArrayFormItemFromSwagger = ({
{(fields, { add, remove }, { errors }) => (
<>
{fields.map(field => {
- const fieldType = (
+ const rawFieldType = (
schema.items as (OpenAPIV2.ItemsObject & { properties?: OpenAPIV2.SchemaObject }) | undefined
)?.type
+ // Handle type as string or string[] (OpenAPI v2 allows both)
+ const fieldType = Array.isArray(rawFieldType) ? rawFieldType[0] : rawFieldType
const description = (schema.items as (OpenAPIV2.ItemsObject & { description?: string }) | undefined)
?.description
const entry = schema.items as
@@ -577,7 +579,29 @@ export const getArrayFormItemFromSwagger = ({
type="text"
size="small"
onClick={() => {
- add()
+ // Determine initial value based on item type
+ const fieldType = (
+ schema.items as (OpenAPIV2.ItemsObject & { properties?: OpenAPIV2.SchemaObject }) | undefined
+ )?.type
+
+ let initialValue: unknown
+ // Handle type as string or string[] (OpenAPI v2 allows both)
+ const typeStr = Array.isArray(fieldType) ? fieldType[0] : fieldType
+ if (typeStr === 'string') {
+ initialValue = ''
+ } else if (typeStr === 'number' || typeStr === 'integer') {
+ initialValue = 0
+ } else if (typeStr === 'boolean') {
+ initialValue = false
+ } else if (typeStr === 'array') {
+ initialValue = []
+ } else if (typeStr === 'object') {
+ initialValue = {}
+ } else {
+ initialValue = ''
+ }
+
+ add(initialValue)
}}
>
<PlusIcon />

View File

@@ -1,16 +1,18 @@
diff --git a/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx b/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx
index b6fb99f..965bac0 100644
index ac56e5f..c6e2350 100644
--- a/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx
+++ b/src/components/organisms/ListInsideClusterAndNs/ListInsideClusterAndNs.tsx
@@ -1,11 +1,16 @@
@@ -1,6 +1,6 @@
import React, { FC, useState } from 'react'
import { Button, Alert, Spin, Typography } from 'antd'
-import { filterSelectOptions, Spacer, useBuiltinResources } from '@prorobotech/openapi-k8s-toolkit'
-import { filterSelectOptions, Spacer, useBuiltinResources, useApiResources } from '@prorobotech/openapi-k8s-toolkit'
+import { filterSelectOptions, Spacer, useApiResources } from '@prorobotech/openapi-k8s-toolkit'
import { useNavigate } from 'react-router-dom'
import { useSelector, useDispatch } from 'react-redux'
import { RootState } from 'store/store'
import { setCluster } from 'store/cluster/cluster/cluster'
@@ -11,6 +11,11 @@ import {
CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME,
} from 'constants/customizationApiGroupAndVersion'
import { Styled } from './styled'
+import {
+ BASE_PROJECTS_API_GROUP,
@@ -20,9 +22,9 @@ index b6fb99f..965bac0 100644
export const ListInsideClusterAndNs: FC = () => {
const clusterList = useSelector((state: RootState) => state.clusterList.clusterList)
@@ -17,9 +22,11 @@ export const ListInsideClusterAndNs: FC = () => {
const [selectedCluster, setSelectedCluster] = useState<string>()
const [selectedNamespace, setSelectedNamespace] = useState<string>()
@@ -33,9 +38,11 @@ export const ListInsideClusterAndNs: FC = () => {
typeof CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME === 'string' &&
CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME.length > 0
- const namespacesData = useBuiltinResources({
+ const namespacesData = useApiResources({
@@ -32,7 +34,7 @@ index b6fb99f..965bac0 100644
+ apiVersion: BASE_PROJECTS_VERSION,
+ typeName: BASE_PROJECTS_RESOURCE_NAME,
limit: null,
isEnabled: selectedCluster !== undefined,
isEnabled: selectedCluster !== undefined && !isCustomNamespaceResource,
})
diff --git a/src/hooks/useNavSelectorInside.ts b/src/hooks/useNavSelectorInside.ts
index 5736e2b..1ec0f71 100644

File diff suppressed because one or more lines are too long

View File

@@ -42,6 +42,8 @@ spec:
value: dashboard.cozystack.io
- name: BASE_API_VERSION
value: v1alpha1
- name: BASE_NAMESPACE_FULL_PATH
value: "/apis/core.cozystack.io/v1alpha1/tenantnamespaces"
- name: LOGGER
value: "TRUE"
- name: LOGGER_WITH_HEADERS
@@ -122,6 +124,12 @@ spec:
value: tenantnamespaces
- name: PROJECTS_VERSION
value: v1alpha1
- name: CUSTOM_NAMESPACE_API_RESOURCE_API_GROUP
value: core.cozystack.io
- name: CUSTOM_NAMESPACE_API_RESOURCE_API_VERSION
value: v1alpha1
- name: CUSTOM_NAMESPACE_API_RESOURCE_RESOURCE_NAME
value: tenantnamespaces
- name: USE_NAMESPACE_NAV
value: "true"
- name: LOGIN_URL
@@ -140,21 +148,21 @@ spec:
configMapKeyRef:
name: incloud-web-dashboard-config
key: TITLE_TEXT
- name: TENANT_TEXT
- name: CUSTOM_TENANT_TEXT
valueFrom:
configMapKeyRef:
name: incloud-web-dashboard-config
key: TENANT_TEXT
key: CUSTOM_TENANT_TEXT
- name: LOGO_TEXT
valueFrom:
configMapKeyRef:
name: incloud-web-dashboard-config
key: LOGO_TEXT
- name: LOGO_SVG
- name: CUSTOM_LOGO_SVG
valueFrom:
configMapKeyRef:
name: incloud-web-dashboard-config
key: LOGO_SVG
key: CUSTOM_LOGO_SVG
- name: ICON_SVG
valueFrom:
configMapKeyRef:

View File

@@ -1,6 +1,6 @@
openapiUI:
image: ghcr.io/cozystack/cozystack/openapi-ui:latest@sha256:dfd3227aec5944e303a96f8e3197ac37a6b6bd7994ddbd681b7d5827bd621f4b
image: ghcr.io/cozystack/cozystack/openapi-ui:latest@sha256:b942d98ff0ea36e3c6e864b6459b404d37ed68bc2b0ebc5d3007a1be4faf60c5
openapiUIK8sBff:
image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:latest@sha256:d2200791865a84640722079f2a3194af0c67d83c27c1bd303215183450265485
image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:latest@sha256:5ddc6546baf3acdb8e0572536665fe73053a7f985b05e51366454efa11c201d2
tokenProxy:
image: ghcr.io/cozystack/cozystack/token-proxy:v0.37.0@sha256:fad27112617bb17816702571e1f39d0ac3fe5283468d25eb12f79906cdab566b
image: ghcr.io/cozystack/cozystack/token-proxy:latest@sha256:fad27112617bb17816702571e1f39d0ac3fe5283468d25eb12f79906cdab566b