Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
06ecb8a
feat(console): add RJSF custom templates and widgets
Apr 23, 2026
c430e41
feat(console): improve secrets display with formatting
Apr 23, 2026
dda377a
feat(console): add conditional tabs based on application kind
Apr 23, 2026
8e8172e
feat(console): add VNC tab with noVNC viewer (WIP)
Apr 23, 2026
670c9d8
feat(console): add StorageClassWidget for storage class selection
Apr 23, 2026
286c79e
refactor(console): improve RJSF form widgets
Apr 23, 2026
7a571b8
fix(k8s-client): add labelSelector support to watch requests
Apr 23, 2026
bed1285
feat(console): add StorageClass dropdown widget
Apr 23, 2026
1c08c51
feat(console): improve tenants management and UI
Apr 23, 2026
2ac7daf
feat(console): add StorageClassWidget for automatic storage class sel…
Apr 23, 2026
c682948
fix(console): resolve noVNC module import issue in VNC viewer
Apr 23, 2026
af57a4c
refactor(console): improve tenants page with namespace-based display
Apr 23, 2026
08a12a0
style(console): simplify UI components styling
Apr 23, 2026
2581552
refactor(k8s-client): remove unused watch parameters
Apr 23, 2026
9b59c34
feat(console): customize VMInstance tabs layout
Apr 23, 2026
c13e3f5
fix(k8s-client): add labelSelector and fieldSelector support to watch
Apr 23, 2026
c82f016
fix(console): correct label filtering for application resources
Apr 23, 2026
4145d51
refactor(console): simplify label selectors and switch to tenant secrets
Apr 23, 2026
d06d8aa
refactor(k8s-client): remove unused watch search parameters
Apr 23, 2026
57a3cee
feat(console): improve secrets display with collapsible sections
Apr 23, 2026
382e25a
feat(console): add reveal all keys button for secrets
Apr 23, 2026
551a513
fix(ui): enable vertical scrolling in sidebar navigation
Apr 23, 2026
0ba021c
feat(console): add tenant editing and improve object field detection
Apr 23, 2026
aa9809a
fix(console): correct tenant modules display logic
Apr 23, 2026
a91ea96
feat(backups): add backup management UI with dynamic forms
Apr 23, 2026
46fb07c
refactor(tenants): improve module detection using labels
Apr 23, 2026
ef98742
fix(tenants): fetch modules from TenantModule CRD instead of labels
Apr 23, 2026
86635e7
feat(secrets): add tenant-specific secret handling
Apr 24, 2026
a705520
feat(forms): add additionalProperties field editor and improve UX
Apr 24, 2026
0ba8df1
feat(backups): generate schemas from CRD and add edit/delete function…
Apr 24, 2026
065e476
feat(backups): add specialized Backup creation page and improve form …
Apr 24, 2026
8b6579e
feat(forms): add conditional rendering for addon objects
Apr 24, 2026
4849186
style(forms): replace "Chart Values" title with "Parameters"
Apr 24, 2026
5365b8b
feat(forms): improve empty option display in SourceWidget
Apr 24, 2026
9666eab
fix(forms): use SourceField instead of SourceWidget for proper rendering
Apr 24, 2026
361d062
feat(detail): add auto-refresh for application instance data
Apr 24, 2026
c77dd39
fix(detail): redirect to resource list after deletion instead of cons…
Apr 24, 2026
5a3945b
fix(console): keep console context when deploying new resources
Apr 24, 2026
b2a2933
fix(console): use correct deploy URL for console context
Apr 24, 2026
14fe3c9
feat(console): add application creation route in console context
Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@cozystack/types": "workspace:*",
"@cozystack/ui": "workspace:*",
"@monaco-editor/react": "^4.7.0",
"@novnc/novnc": "^1.6.0",
"@rjsf/core": "^5.24.8",
"@rjsf/utils": "^5.24.8",
"@rjsf/validator-ajv8": "^5.24.8",
Expand Down
131 changes: 131 additions & 0 deletions apps/console/src/components/AdditionalPropertiesEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { useState } from "react"
import Form from "@rjsf/core"
import validator from "@rjsf/validator-ajv8"
import type { RJSFSchema } from "@rjsf/utils"
import { customTemplates, customWidgets } from "./rjsf-templates.tsx"

interface AdditionalPropertiesEditorProps {
value: Record<string, unknown>
onChange: (value: Record<string, unknown>) => void
readonly?: boolean
title?: string
description?: string
required?: boolean
itemSchema: RJSFSchema
}

export function AdditionalPropertiesEditor({
value,
onChange,
readonly,
title,
description,
required,
itemSchema,
}: AdditionalPropertiesEditorProps) {
const [newKey, setNewKey] = useState("")

const keys = Object.keys(value || {})

const handleAddKey = () => {
if (!newKey.trim()) return
if (value && newKey in value) {
alert(`Key "${newKey}" already exists`)
return
}

// Initialize with default value from schema
const defaultValue = itemSchema.type === "object" ? {} : itemSchema.default ?? ""
onChange({ ...value, [newKey]: defaultValue })
setNewKey("")
}

const handleRemoveKey = (key: string) => {
const newValue = { ...value }
delete newValue[key]
onChange(newValue)
}

const handleValueChange = (key: string, newVal: unknown) => {
onChange({ ...value, [key]: newVal })
}

return (
<div className="field">
{title && (
<label className="control-label mb-2 block text-sm font-medium text-slate-700">
{title}
{required && <span className="required ml-1 text-red-500">*</span>}
</label>
)}
{description && <p className="field-description mb-3 text-xs text-slate-500">{description}</p>}

<div className="space-y-3">
{keys.map((key) => (
<div key={key} className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<div className="mb-2 flex items-center justify-between">
<div className="font-mono text-sm font-semibold text-slate-900">{key}</div>
{!readonly && (
<button
type="button"
onClick={() => handleRemoveKey(key)}
className="rounded-md border border-red-300 bg-white px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
>
× Remove
</button>
)}
</div>
<div className="rounded-md bg-white p-3">
<Form
schema={itemSchema}
formData={value[key]}
validator={validator}
templates={customTemplates}
widgets={customWidgets}
onChange={(e) => handleValueChange(key, e.formData)}
disabled={readonly}
liveValidate={false}
showErrorList={false}
uiSchema={{
"ui:submitButtonOptions": { norender: true },
}}
>
{/* No submit button */}
</Form>
</div>
</div>
))}

{keys.length === 0 && readonly && (
<div className="text-sm text-slate-500 italic">No entries</div>
)}

{!readonly && (
<div className="flex gap-2">
<input
type="text"
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
handleAddKey()
}
}}
placeholder="Enter key name..."
className="flex-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
/>
<button
type="button"
onClick={handleAddKey}
disabled={!newKey.trim()}
className="rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
+ Add
</button>
</div>
)}
</div>
</div>
)
}
126 changes: 126 additions & 0 deletions apps/console/src/components/AdditionalPropertiesField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { useState, useMemo } from "react"
import type { FieldProps, RJSFSchema, TemplatesType } from "@rjsf/utils"
import Form from "@rjsf/core"
import validator from "@rjsf/validator-ajv8"
import { customTemplates, customWidgets } from "./rjsf-templates.tsx"

export function AdditionalPropertiesField(props: FieldProps) {
const { schema, formData, onChange, readonly, disabled, name, required } = props
const [newKey, setNewKey] = useState("")

// Get the schema for items from additionalProperties
const itemSchema = (schema.additionalProperties as RJSFSchema) || {}
const keys = Object.keys(formData || {})

// Create templates without submit button for nested forms
const templatesWithoutSubmit = useMemo<Partial<TemplatesType>>(() => {
return {
...customTemplates,
ButtonTemplates: {
...customTemplates.ButtonTemplates,
SubmitButton: () => null,
},
}
}, [])

const handleAddKey = () => {
if (!newKey.trim()) return
if (formData && newKey in formData) {
alert(`Key "${newKey}" already exists`)
return
}

// Initialize with default value from schema
const defaultValue = itemSchema.type === "object" ? {} : itemSchema.default ?? ""
onChange({ ...formData, [newKey]: defaultValue })
setNewKey("")
}

const handleRemoveKey = (key: string) => {
const newValue = { ...formData }
delete newValue[key]
onChange(newValue)
}

const handleValueChange = (key: string, newVal: unknown) => {
onChange({ ...formData, [key]: newVal })
}

const isReadonly = readonly || disabled

return (
<div className="form-group field">
{name && (
<label className="control-label mb-2 block text-sm font-medium text-slate-700">
{name}
{required && <span className="required ml-1 text-red-500">*</span>}
</label>
)}
{schema.description && (
<p className="field-description mb-3 text-xs text-slate-500">{schema.description}</p>
)}

<div className="space-y-3">
{keys.map((key) => (
<div key={key} className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<div className="mb-2 flex items-center justify-between">
<div className="font-mono text-sm font-semibold text-slate-900">{key}</div>
{!isReadonly && (
<button
type="button"
onClick={() => handleRemoveKey(key)}
className="rounded-md border border-red-300 bg-white px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
>
× Remove
</button>
)}
</div>
<div className="rounded-md bg-white p-3">
<Form
schema={itemSchema}
formData={formData[key]}
validator={validator}
templates={templatesWithoutSubmit}
widgets={customWidgets}
onChange={(e) => handleValueChange(key, e.formData)}
disabled={isReadonly}
liveValidate={false}
showErrorList={false}
/>
</div>
</div>
))}

{keys.length === 0 && isReadonly && (
<div className="text-sm text-slate-500 italic">No entries</div>
)}

{!isReadonly && (
<div className="flex gap-2">
<input
type="text"
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
handleAddKey()
}
}}
placeholder="Enter key name..."
className="flex-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
/>
<button
type="button"
onClick={handleAddKey}
disabled={!newKey.trim()}
className="rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
+ Add
</button>
</div>
)}
</div>
</div>
)
}
120 changes: 120 additions & 0 deletions apps/console/src/components/AdditionalPropertiesWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { useState } from "react"
import type { WidgetProps, RJSFSchema } from "@rjsf/utils"
import Form from "@rjsf/core"
import validator from "@rjsf/validator-ajv8"
import { customTemplates, customWidgets } from "./rjsf-templates.tsx"

export function AdditionalPropertiesWidget(props: WidgetProps) {
const { schema, value, onChange, readonly, disabled, label, required } = props
const [newKey, setNewKey] = useState("")

// Get the schema for items from additionalProperties
const itemSchema = (schema.additionalProperties as RJSFSchema) || {}
const keys = Object.keys(value || {})

const handleAddKey = () => {
if (!newKey.trim()) return
if (value && newKey in value) {
alert(`Key "${newKey}" already exists`)
return
}

// Initialize with default value from schema
const defaultValue = itemSchema.type === "object" ? {} : itemSchema.default ?? ""
onChange({ ...value, [newKey]: defaultValue })
setNewKey("")
}

const handleRemoveKey = (key: string) => {
const newValue = { ...value }
delete newValue[key]
onChange(newValue)
}

const handleValueChange = (key: string, newVal: unknown) => {
onChange({ ...value, [key]: newVal })
}

const isReadonly = readonly || disabled

return (
<div className="field">
{label && (
<label className="control-label mb-2 block text-sm font-medium text-slate-700">
{label}
{required && <span className="required ml-1 text-red-500">*</span>}
</label>
)}
{schema.description && (
<p className="field-description mb-3 text-xs text-slate-500">{schema.description}</p>
)}

<div className="space-y-3">
{keys.map((key) => (
<div key={key} className="rounded-lg border border-slate-200 bg-slate-50 p-4">
<div className="mb-2 flex items-center justify-between">
<div className="font-mono text-sm font-semibold text-slate-900">{key}</div>
{!isReadonly && (
<button
type="button"
onClick={() => handleRemoveKey(key)}
className="rounded-md border border-red-300 bg-white px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50"
>
× Remove
</button>
)}
</div>
<div className="rounded-md bg-white p-3">
<Form
schema={itemSchema}
formData={value[key]}
validator={validator}
templates={customTemplates}
widgets={customWidgets}
onChange={(e) => handleValueChange(key, e.formData)}
disabled={isReadonly}
liveValidate={false}
showErrorList={false}
uiSchema={{
"ui:submitButtonOptions": { norender: true },
}}
>
{/* No submit button */}
</Form>
</div>
</div>
))}

{keys.length === 0 && isReadonly && (
<div className="text-sm text-slate-500 italic">No entries</div>
)}

{!isReadonly && (
<div className="flex gap-2">
<input
type="text"
value={newKey}
onChange={(e) => setNewKey(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault()
handleAddKey()
}
}}
placeholder="Enter key name..."
className="flex-1 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder:text-slate-400 outline-none focus:border-blue-400 focus:ring-1 focus:ring-blue-400"
/>
<button
type="button"
onClick={handleAddKey}
disabled={!newKey.trim()}
className="rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
+ Add
</button>
</div>
)}
</div>
</div>
)
}
Loading