diff --git a/apps/console/src/components/CustomObjectFieldTemplate.tsx b/apps/console/src/components/CustomObjectFieldTemplate.tsx index b8f09cb..c51b5e7 100644 --- a/apps/console/src/components/CustomObjectFieldTemplate.tsx +++ b/apps/console/src/components/CustomObjectFieldTemplate.tsx @@ -1,10 +1,46 @@ import type { ObjectFieldTemplateProps, + ObjectFieldTemplatePropertyType, RJSFSchema, StrictRJSFSchema, FormContextType, } from "@rjsf/utils" +function isSimpleField(schema: any): boolean { + if (!schema) return true + const type = schema.type + if (type === "object") return false + if (type === "array") { + const itemType = schema.items?.type + return itemType === "integer" || itemType === "string" || itemType === "number" + } + if (schema.anyOf || schema.oneOf || schema.allOf) { + if (schema["x-kubernetes-int-or-string"]) return true + return false + } + return true +} + +function groupByComplexity( + properties: ObjectFieldTemplatePropertyType[], + parentSchema: any, +) { + type Group = { simple: boolean; items: ObjectFieldTemplatePropertyType[] } + const groups: Group[] = [] + let current: Group | null = null + + for (const prop of properties) { + const fieldSchema = parentSchema?.properties?.[prop.name] + const simple = isSimpleField(fieldSchema) + if (!current || current.simple !== simple) { + current = { simple, items: [] } + groups.push(current) + } + current.items.push(prop) + } + return groups +} + export function CustomObjectFieldTemplate< T = any, S extends StrictRJSFSchema = RJSFSchema, @@ -12,42 +48,48 @@ export function CustomObjectFieldTemplate< >(props: ObjectFieldTemplateProps) { const { formData } = props - // Check if this is an addon object (has 'enabled' field and other config fields) + // Addon pattern: has 'enabled' + other config fields → conditional expand const hasEnabledField = props.properties.some((p) => p.name === "enabled") const hasOtherFields = props.properties.some((p) => p.name !== "enabled") const isAddon = hasEnabledField && hasOtherFields - // If this is an addon, use conditional rendering based on 'enabled' state if (isAddon) { const isEnabled = (formData as any)?.enabled === true const enabledProp = props.properties.find((p) => p.name === "enabled") const otherProps = props.properties.filter((p) => p.name !== "enabled") + const groups = groupByComplexity(otherProps, props.schema) return ( -
+
{props.title && ( - {props.title} + {props.title} )} - {props.description &&

{props.description}

} - - {/* Always show the 'enabled' checkbox */} - {enabledProp && ( -
- {enabledProp.content} -
+ {props.description && ( +

{props.description}

)} - {/* Show other fields only if enabled */} + {enabledProp &&
{enabledProp.content}
} + {isEnabled && otherProps.length > 0 && ( -
- {otherProps.map((prop) => ( -
{prop.content}
- ))} +
+ {groups.map((group, i) => + group.simple ? ( +
+ {group.items.map((prop) => ( +
{prop.content}
+ ))} +
+ ) : ( + group.items.map((prop) => ( +
{prop.content}
+ )) + ) + )}
)} {!isEnabled && otherProps.length > 0 && ( -

+

Enable this addon to configure additional settings

)} @@ -55,12 +97,26 @@ export function CustomObjectFieldTemplate< ) } - // Otherwise, use default rendering + // Default: smart 2-column grid for simple fields, full width for complex + const groups = groupByComplexity(props.properties, props.schema) + return (
{props.title && {props.title}} {props.description &&

{props.description}

} - {props.properties.map((prop) => prop.content)} + {groups.map((group, i) => + group.simple ? ( +
+ {group.items.map((prop) => ( +
{prop.content}
+ ))} +
+ ) : ( + group.items.map((prop) => ( +
{prop.content}
+ )) + ) + )}
) } diff --git a/apps/console/src/components/command-palette/command-palette.tsx b/apps/console/src/components/command-palette/command-palette.tsx index 02e3842..808011e 100644 --- a/apps/console/src/components/command-palette/command-palette.tsx +++ b/apps/console/src/components/command-palette/command-palette.tsx @@ -126,18 +126,18 @@ export function CommandPalette() { aria-label="Command palette" > {/* Header with optional back + search */} -
+
{breadcrumb ? ( ) : ( - + )} {!breadcrumb && ( - - {isMac ? : Ctrl+}K + + {isMac ? "⌘K" : "Ctrl+K"} )}
{/* Results list */}
{items.length === 0 && !isLoading && ( -
+
No results found.
)} {isLoading && items.length === 0 && ( -
- Loading... +
+ Loading…
)} {grouped.map((group) => (
{group.label && ( -
+
{group.label}
)} @@ -223,8 +223,8 @@ const PaletteItem = forwardRef< role="option" aria-selected={highlighted} className={cn( - "relative flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none select-none", - highlighted && "bg-slate-100 text-slate-900" + "relative flex cursor-default items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm outline-none select-none transition-colors", + highlighted ? "bg-blue-50 text-blue-900" : "text-slate-700 hover:bg-slate-50" )} onClick={onSelect} > @@ -233,14 +233,14 @@ const PaletteItem = forwardRef< {icon} )} - {label} + {label} {description && ( - + {description} )} {drilldown && ( - + )}
) diff --git a/apps/console/src/components/rjsf-templates.tsx b/apps/console/src/components/rjsf-templates.tsx index 91b56f7..bfd9635 100644 --- a/apps/console/src/components/rjsf-templates.tsx +++ b/apps/console/src/components/rjsf-templates.tsx @@ -1,5 +1,6 @@ import type { IconButtonProps, + ArrayFieldTemplateItemType, TemplatesType, FormContextType, RJSFSchema, @@ -31,16 +32,46 @@ function IconButton< } const buttonClassName = - "rounded-md border border-slate-300 bg-white px-3 py-1.5 text-sm font-medium text-slate-700 hover:bg-slate-50" + "rounded-md border border-slate-200 bg-white px-3 py-1.5 text-sm font-medium text-slate-600 shadow-sm hover:bg-slate-50 hover:border-slate-300 transition-colors" const removeButtonClassName = - "rounded-md border border-red-300 bg-white px-3 py-1.5 text-sm font-medium text-red-600 hover:bg-red-50" + "rounded-md border border-red-200 bg-white px-3 py-1.5 text-sm font-medium text-red-600 shadow-sm hover:bg-red-50 hover:border-red-300 transition-colors" + +function ArrayFieldItemTemplate< + T = any, + S extends StrictRJSFSchema = RJSFSchema, + F extends FormContextType = any, +>(props: ArrayFieldTemplateItemType) { + const { children, hasRemove, index, onDropIndexClick, disabled, readonly, schema } = props + const isNumeric = (schema as any).type === 'integer' || (schema as any).type === 'number' + return ( +
+
{children}
+ {hasRemove && ( + + )} +
+ ) +} export const customTemplates = { ObjectFieldTemplate: CustomObjectFieldTemplate, + ArrayFieldItemTemplate: ArrayFieldItemTemplate, ButtonTemplates: { AddButton: (props: IconButtonProps) => ( - + ), RemoveButton: (props: IconButtonProps) => ( diff --git a/apps/console/src/components/schema-form.css b/apps/console/src/components/schema-form.css index 1a29fa3..9d62a61 100644 --- a/apps/console/src/components/schema-form.css +++ b/apps/console/src/components/schema-form.css @@ -9,10 +9,10 @@ } .rjsf-container .field > label, .rjsf-container .control-label { - @apply mb-1 block text-sm font-medium text-slate-700; + @apply mb-1 block text-xs font-medium text-slate-600; } .rjsf-container .field-description { - @apply mt-0.5 text-xs text-slate-500; + @apply mt-0.5 text-xs text-slate-400; } .rjsf-container .required { @apply ml-0.5 text-red-500; @@ -21,22 +21,22 @@ .rjsf-container input[type="number"], .rjsf-container input[type="email"], .rjsf-container textarea { - @apply w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400; + @apply w-full rounded-md border border-slate-200 bg-white px-2.5 py-1.5 text-sm text-slate-900 shadow-sm outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20; } .rjsf-container select { - @apply w-full rounded-lg border border-slate-300 bg-white pl-3 pr-8 py-2 text-sm text-slate-900 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400; + @apply w-full rounded-md border border-slate-200 bg-white pl-2.5 pr-8 py-1.5 text-sm text-slate-900 shadow-sm outline-none transition-shadow focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20; } .rjsf-container input[type="checkbox"] { - @apply mr-2 size-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500; + @apply mr-2 size-4 rounded border-slate-300 accent-blue-600 focus:ring-2 focus:ring-blue-500/20; } .rjsf-container fieldset { - @apply mt-2 mb-4 rounded-lg border border-slate-200 bg-white p-4; + @apply mt-1.5 mb-3 rounded-lg border border-slate-200 bg-white p-3 shadow-sm; } .rjsf-container fieldset > legend { - @apply -ml-1 px-1 text-sm font-semibold text-slate-900; + @apply -ml-1 px-1 text-xs font-semibold text-slate-700; } .rjsf-container fieldset fieldset { - @apply bg-slate-50; + @apply bg-slate-50/70 shadow-none p-2.5; } .rjsf-container .btn-add, .rjsf-container .object-property-expand button, @@ -44,8 +44,67 @@ .rjsf-container .array-item-remove button, .rjsf-container .array-item-move-up button, .rjsf-container .array-item-move-down button { - @apply rounded-md border border-slate-300 bg-white px-2 py-1 text-xs font-medium text-slate-700 hover:bg-slate-50; + @apply rounded-md border border-slate-200 bg-white px-2.5 py-1 text-xs font-medium text-slate-600 shadow-sm hover:bg-slate-50 hover:border-slate-300 transition-colors; } .rjsf-container .text-danger { - @apply text-xs text-red-600; + @apply text-xs text-red-500; +} +/* Hide auto-generated labels for primitive array items (e.g. "fieldName-1") */ +.rjsf-container .field-array-of-integer .control-label, +.rjsf-container .field-array-of-string .control-label, +.rjsf-container .field-array-of-number .control-label { + display: none; +} +/* Remove bottom margin from fields inside array item rows (template handles spacing) */ +.rjsf-container .array-item-list .field { + margin-bottom: 0; +} +/* Compact fields inside simple-field grid columns */ +.rjsf-container .grid-fields { + padding-left: 7px; +} +.rjsf-container .grid-fields .field { + margin-bottom: 0.5rem; +} +.rjsf-container .grid-fields .field-description { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + cursor: default; +} +/* Boolean fields lack a control-label — add equivalent top offset so they align with adjacent fields */ +.rjsf-container .grid-fields .field-boolean { + padding-top: 1.25rem; +} +/* Array item row: hover highlight */ +.rjsf-container .array-item-row { + border-radius: 0.375rem; + padding: 0.125rem 0.375rem; + margin-left: -0.375rem; + margin-right: -0.375rem; + transition: background-color 0.1s ease; +} +.rjsf-container .array-item-row:hover { + background-color: rgb(248 250 252); +} +/* Monospace + muted-code treatment for numeric array inputs (ports, counts, etc.) */ +.rjsf-container .field-array-of-integer input[type="number"], +.rjsf-container .field-array-of-number input[type="number"] { + font-family: ui-monospace, 'SF Mono', Menlo, monospace; + font-size: 0.8rem; + letter-spacing: 0.03em; + background-color: rgb(248 250 252); +} +/* Spinner arrows hidden for port-number inputs — value is exact, arrows add noise */ +.rjsf-container .field-array-of-integer input[type="number"]::-webkit-inner-spin-button, +.rjsf-container .field-array-of-integer input[type="number"]::-webkit-outer-spin-button, +.rjsf-container .field-array-of-number input[type="number"]::-webkit-inner-spin-button, +.rjsf-container .field-array-of-number input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} +.rjsf-container .field-array-of-integer input[type="number"], +.rjsf-container .field-array-of-number input[type="number"] { + -moz-appearance: textfield; } diff --git a/apps/console/src/components/ui/dialog.tsx b/apps/console/src/components/ui/dialog.tsx index 8188823..fb6980d 100644 --- a/apps/console/src/components/ui/dialog.tsx +++ b/apps/console/src/components/ui/dialog.tsx @@ -15,7 +15,7 @@ function DialogBackdrop({ -): unknown { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { if (!schema || typeof schema !== "object") return schema const currentPath = path.join(".") diff --git a/apps/console/src/lib/use-crd-schema.ts b/apps/console/src/lib/use-crd-schema.ts index 05404bb..23a4031 100644 --- a/apps/console/src/lib/use-crd-schema.ts +++ b/apps/console/src/lib/use-crd-schema.ts @@ -2,6 +2,7 @@ import { useK8sGet } from "@cozystack/k8s-client" interface CRDVersion { name: string + storage?: boolean schema?: { openAPIV3Schema?: { properties?: { diff --git a/apps/console/src/routes/ApplicationOrderPage.tsx b/apps/console/src/routes/ApplicationOrderPage.tsx index b1e7d01..63f1b97 100644 --- a/apps/console/src/routes/ApplicationOrderPage.tsx +++ b/apps/console/src/routes/ApplicationOrderPage.tsx @@ -172,27 +172,29 @@ export function ApplicationOrderPage({
-
+
{mode === "form" ? ( -
-
-
-