-
Notifications
You must be signed in to change notification settings - Fork 0
fix(backup): several fixes around backup-pages #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,247 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useMemo } from "react" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useNavigate } from "react-router" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Archive, Save } from "lucide-react" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Button, Section, Spinner } from "@cozystack/ui" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useK8sCreate, useK8sList } from "@cozystack/k8s-client" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useTenantContext } from "../lib/tenant-context.tsx" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useApplicationDefinitions } from "../lib/app-definitions.ts" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCRDSchema } from "../lib/use-crd-schema.ts" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { SchemaForm } from "../components/SchemaForm.tsx" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { enrichSchemaWithEnums } from "../lib/backup-utils.ts" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function BackupJobCreatePage() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const navigate = useNavigate() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { tenantNamespace } = useTenantContext() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: appDefs } = useApplicationDefinitions() | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [formData, setFormData] = useState<any>({}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [name, setName] = useState("") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get base schema from CRD | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { schema: baseSchema, isLoading: schemaLoading } = useCRDSchema( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| "backupjobs.backups.cozystack.io" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get BackupClasses (cluster-scoped) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: backupClassesData } = useK8sList<any>({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiGroup: "backups.cozystack.io", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiVersion: "v1alpha1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| plural: "backupclasses", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get Plans in the tenant namespace (optional reference) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: plansData } = useK8sList<any>({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiGroup: "backups.cozystack.io", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiVersion: "v1alpha1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| plural: "plans", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace: tenantNamespace ?? "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, { enabled: !!tenantNamespace }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Resolve instances for the selected application kind. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mirrors BackupRestoreJobCreatePage: kind dropdown is gated to | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // apps.cozystack.io (the only apiGroup ApplicationDefinitions cover). | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Strict undefined check so an explicit empty string from the user means | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // "no group" — clearing the field opts out of the cozystack defaults. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const selectedKind = formData?.applicationRef?.kind | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawApiGroup = formData?.applicationRef?.apiGroup | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const selectedApiGroup = rawApiGroup === undefined ? "apps.cozystack.io" : rawApiGroup | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const selectedAppDef = useMemo( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| () => appDefs?.items.find(d => d.spec?.application.kind === selectedKind), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| [appDefs, selectedKind] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: instancesData } = useK8sList<any>({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiGroup: "apps.cozystack.io", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiVersion: "v1alpha1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| plural: selectedAppDef?.spec?.application.plural ?? "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace: tenantNamespace ?? "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, { enabled: !!selectedAppDef && !!tenantNamespace && selectedApiGroup === "apps.cozystack.io" }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const createMutation = useK8sCreate({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiGroup: "backups.cozystack.io", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiVersion: "v1alpha1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| plural: "backupjobs", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace: tenantNamespace ?? "", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const schema = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!baseSchema) return null | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const base = JSON.parse(baseSchema) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const kinds: string[] = selectedApiGroup === "apps.cozystack.io" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? appDefs?.items.map(d => d.spec?.application.kind).filter((k): k is string => Boolean(k)) ?? [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| : [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const instances = instancesData?.items.map((inst: any) => inst.metadata.name) ?? [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const backupClasses = backupClassesData?.items.map((bc: any) => bc.metadata.name) ?? [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const plans = plansData?.items.map((p: any) => p.metadata.name) ?? [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const enumMap: Record<string, string[]> = {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (kinds.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| enumMap["applicationRef.kind"] = kinds | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (selectedApiGroup === "apps.cozystack.io" && selectedKind && instances.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| enumMap["applicationRef.name"] = instances | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (backupClasses.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| enumMap["backupClassName"] = backupClasses | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (plans.length > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // planRef is optional in the CRD (default ""). Prepend an empty value | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // so the dropdown opens with no plan selected — matches the CRD default | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // and avoids accidentally pinning the BackupJob to the first listed Plan. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| enumMap["planRef.name"] = ["", ...plans] | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const enriched = enrichSchemaWithEnums(base, [], enumMap) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Default the optional apiGroup to apps.cozystack.io so the cozystack- | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // managed kinds match without the user typing the group manually. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (enriched.properties?.applicationRef?.properties?.apiGroup) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| enriched.properties.applicationRef.properties.apiGroup.default = "apps.cozystack.io" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return JSON.stringify(enriched) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [baseSchema, appDefs, backupClassesData, plansData, instancesData, selectedKind, selectedApiGroup]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSubmit = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!tenantNamespace) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert("Tenant namespace is not available. Please refresh.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!name.trim()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert("Name is required") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!formData.applicationRef?.kind || !formData.applicationRef?.name) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert("Application reference is required") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!formData.backupClassName) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert("Backup class name is required") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| // planRef is optional metadata recording which Plan triggered the job. The | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // dropdown ships an empty sentinel; strip it so the API never receives | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // `planRef: { name: "" }`, which would otherwise round-trip as a malformed | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| // LocalObjectReference. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| const spec = { ...formData } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!spec.planRef?.name) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| delete spec.planRef | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const resource = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiVersion: "backups.cozystack.io/v1alpha1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| kind: "BackupJob", | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: name.trim(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| namespace: tenantNamespace ?? undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| spec, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| await createMutation.mutateAsync(resource) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| navigate("/console/backups/backupjobs") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert(`Failed to create BackupJob: ${(err as Error).message}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+107
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleCancel = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| navigate("/console/backups/backupjobs") | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (schemaLoading) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2 p-8 text-slate-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Spinner /> Loading schema... | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!schema) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-8 text-red-600"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Failed to load BackupJob schema. Please refresh the page. | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-6"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mb-5 flex items-center gap-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex size-11 shrink-0 items-center justify-center rounded-md bg-slate-100"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Archive className="size-6 text-slate-600" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h1 className="text-lg font-semibold text-slate-900">Create Backup Job</h1> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="text-xs text-slate-500"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Trigger a backup of an application instance | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Section> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="space-y-4 p-5"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label className="block text-sm font-medium text-slate-700 mb-1"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Backup Job Name <span className="text-red-500">*</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={name} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setName(e.target.value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="my-backup-job" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="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" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| required | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+191
to
+201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The 'Backup Job Name' input field is missing an
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <SchemaForm | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| openAPISchema={schema} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| formData={formData} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={setFormData} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="hidden" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </SchemaForm> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2 border-t border-slate-200 px-5 py-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="primary" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleSubmit} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={createMutation.isPending} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| {createMutation.isPending ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Spinner /> Creating... | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Save className="size-3.5" /> Create | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| size="sm" | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleCancel} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={createMutation.isPending} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Cancel | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Section> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,8 +28,12 @@ export function BackupPlanCreatePage() { | |||||||||||||||||
| plural: "backupclasses", | ||||||||||||||||||
| }) | ||||||||||||||||||
|
|
||||||||||||||||||
| // Get instances for selected kind | ||||||||||||||||||
| // Get instances for selected kind. | ||||||||||||||||||
| // Strict undefined check so an explicit empty string from the user means | ||||||||||||||||||
| // "no group" — clearing the field opts out of the cozystack defaults. | ||||||||||||||||||
| const selectedKind = formData?.applicationRef?.kind | ||||||||||||||||||
| const rawApiGroup = formData?.applicationRef?.apiGroup | ||||||||||||||||||
| const selectedApiGroup = rawApiGroup === undefined ? "apps.cozystack.io" : rawApiGroup | ||||||||||||||||||
| const selectedAppDef = useMemo( | ||||||||||||||||||
| () => appDefs?.items.find(d => d.spec?.application.kind === selectedKind), | ||||||||||||||||||
| [appDefs, selectedKind] | ||||||||||||||||||
|
|
@@ -40,7 +44,7 @@ export function BackupPlanCreatePage() { | |||||||||||||||||
| apiVersion: "v1alpha1", | ||||||||||||||||||
| plural: selectedAppDef?.spec?.application.plural ?? "", | ||||||||||||||||||
| namespace: tenantNamespace ?? "", | ||||||||||||||||||
| }, { enabled: !!selectedAppDef && !!tenantNamespace }) | ||||||||||||||||||
| }, { enabled: !!selectedAppDef && !!tenantNamespace && selectedApiGroup === "apps.cozystack.io" }) | ||||||||||||||||||
|
|
||||||||||||||||||
| const createMutation = useK8sCreate({ | ||||||||||||||||||
| apiGroup: "backups.cozystack.io", | ||||||||||||||||||
|
|
@@ -53,7 +57,12 @@ export function BackupPlanCreatePage() { | |||||||||||||||||
| if (!baseSchema) return null | ||||||||||||||||||
|
|
||||||||||||||||||
| const base = JSON.parse(baseSchema) | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||
| const kinds: string[] = appDefs?.items.map(d => d.spec?.application.kind).filter((k): k is string => Boolean(k)) ?? [] | ||||||||||||||||||
| // ApplicationDefinitions are exclusive to apps.cozystack.io — show the | ||||||||||||||||||
| // Kind dropdown only when the selected apiGroup matches; otherwise leave | ||||||||||||||||||
| // it as a free-text input (no enum hint). | ||||||||||||||||||
| const kinds: string[] = selectedApiGroup === "apps.cozystack.io" | ||||||||||||||||||
| ? appDefs?.items.map(d => d.spec?.application.kind).filter((k): k is string => Boolean(k)) ?? [] | ||||||||||||||||||
| : [] | ||||||||||||||||||
| const backupClasses = backupClassesData?.items.map((bc: any) => bc.metadata.name) ?? [] | ||||||||||||||||||
| const instances = instancesData?.items.map((inst: any) => inst.metadata.name) ?? [] | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -63,7 +72,7 @@ export function BackupPlanCreatePage() { | |||||||||||||||||
| if (kinds.length > 0) { | ||||||||||||||||||
| enumMap["applicationRef.kind"] = kinds | ||||||||||||||||||
| } | ||||||||||||||||||
| if (selectedKind && instances.length > 0) { | ||||||||||||||||||
| if (selectedApiGroup === "apps.cozystack.io" && selectedKind && instances.length > 0) { | ||||||||||||||||||
| enumMap["applicationRef.name"] = instances | ||||||||||||||||||
| } | ||||||||||||||||||
| if (backupClasses.length > 0) { | ||||||||||||||||||
|
|
@@ -79,7 +88,7 @@ export function BackupPlanCreatePage() { | |||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| return JSON.stringify(enriched) | ||||||||||||||||||
| }, [baseSchema, appDefs, backupClassesData, instancesData, selectedKind]) | ||||||||||||||||||
| }, [baseSchema, appDefs, backupClassesData, instancesData, selectedKind, selectedApiGroup]) | ||||||||||||||||||
|
|
||||||||||||||||||
| const handleSubmit = async () => { | ||||||||||||||||||
| if (!tenantNamespace) { | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
JSON.parse(baseSchema)call is unsafe and will crash the component ifbaseSchemais invalid or empty. Consider wrapping it in atry-catchblock to handle parsing errors gracefully, similar to the implementation inSchemaForm.tsx.