Skip to content

Commit b4160a8

Browse files
mjudeikisclaude
andcommitted
fix(infrastructure): portal fetches + parses template sampleValues (resilient)
The application template ships spec.sampleValues (PR #343) to pre-fill the provision form for the one-click demo, and ProvisionPage already seeds the form from template.sampleValues — but the portal never received the value, so the form stayed empty. Gaps in api.ts: - neither template GraphQL query (refreshIndex, getTemplate) selected sampleValues, so it was never fetched; - sampleValues is a preserve-unknown-fields field the gateway returns as a JSONString scalar (same as schema), but templateFromGQL cast it straight to an object instead of parsing it. Selecting sampleValues unconditionally is itself a trap: a gateway whose schema was built from an older CRD (pre-sampleValues) rejects the field with a hard "Cannot query field sampleValues" error that breaks the whole catalog/provision page. So select it optimistically via a shared templateSpec() selection and, on that specific error, remember it's unsupported and retry without it (degrading to no pre-fill). Once the updated Template CRD is deployed, the field resolves and the form pre-fills. dist/main.js is gitignored and rebuilt at go build time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cd6a0bc commit b4160a8

1 file changed

Lines changed: 46 additions & 5 deletions

File tree

  • providers/infrastructure/portal/src

providers/infrastructure/portal/src/api.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,18 @@ function templateFromGQL(name: string, spec: Record<string, unknown>): Template
118118
} else if (spec.schema && typeof spec.schema === 'object') {
119119
inputsSchema = spec.schema as JSONSchema
120120
}
121+
// sampleValues is a preserve-unknown-fields field too → same JSONString
122+
// treatment as schema: parse the string form, accept an object as-is.
123+
let sampleValues: Record<string, unknown> | undefined
124+
if (typeof spec.sampleValues === 'string' && spec.sampleValues) {
125+
try {
126+
sampleValues = JSON.parse(spec.sampleValues) as Record<string, unknown>
127+
} catch {
128+
// leave undefined — the form just starts empty
129+
}
130+
} else if (spec.sampleValues && typeof spec.sampleValues === 'object') {
131+
sampleValues = spec.sampleValues as Record<string, unknown>
132+
}
121133
return {
122134
name,
123135
displayName: (spec.displayName as string) || name,
@@ -128,7 +140,7 @@ function templateFromGQL(name: string, spec: Record<string, unknown>): Template
128140
iconURL: spec.iconURL as string | undefined,
129141
kind: instanceCRD.kind ?? '',
130142
inputsSchema,
131-
sampleValues: spec.sampleValues as Record<string, unknown> | undefined,
143+
sampleValues,
132144
}
133145
}
134146

@@ -188,10 +200,39 @@ async function introspectVersionFields(): Promise<Array<{ name: string; typeName
188200
}))
189201
}
190202

203+
// sampleValues is a recent Template field. A gateway whose schema was built from
204+
// an older CRD that predates it has no such field, and selecting an absent field
205+
// is a hard GraphQL error that would break the whole catalog/provision query. So
206+
// select it optimistically and, on that specific error, remember it's missing and
207+
// retry without it (degrading to no form pre-fill). null = not yet probed.
208+
let sampleValuesSupported: boolean | null = null
209+
210+
// templateSpec is the shared Template spec selection set. sampleValues is omitted
211+
// once we've learned the gateway doesn't expose it.
212+
function templateSpec(): string {
213+
const sv = sampleValuesSupported === false ? '' : ' sampleValues'
214+
return `displayName description category version iconURL backend instanceCRD { group version resource kind } schema${sv}`
215+
}
216+
217+
// templateQuery runs a Template query built from templateSpec(), retrying once
218+
// without sampleValues if the gateway rejects that field (older CRD).
219+
async function templateQuery<T>(make: (spec: string) => string, variables: Record<string, unknown> = {}): Promise<T> {
220+
try {
221+
return await graphqlQuery<T>(make(templateSpec()), variables)
222+
} catch (e) {
223+
const msg = (e as { message?: string }).message ?? ''
224+
if (sampleValuesSupported !== false && msg.includes('sampleValues')) {
225+
sampleValuesSupported = false
226+
return await graphqlQuery<T>(make(templateSpec()), variables)
227+
}
228+
throw e
229+
}
230+
}
231+
191232
async function refreshIndex(): Promise<InfraIndex> {
192233
const [tmplData, versionFields] = await Promise.all([
193-
graphqlQuery<Infra<{ Templates?: { items?: Array<{ metadata: { name: string }; spec: Record<string, unknown> }> } }>>(
194-
`{ ${GROUP_FIELD} { ${VERSION} { Templates { items { metadata { name } spec { displayName description category version iconURL backend instanceCRD { group version resource kind } schema } } } } } }`,
234+
templateQuery<Infra<{ Templates?: { items?: Array<{ metadata: { name: string }; spec: Record<string, unknown> }> } }>>(
235+
spec => `{ ${GROUP_FIELD} { ${VERSION} { Templates { items { metadata { name } spec { ${spec} } } } } } }`,
195236
),
196237
introspectVersionFields(),
197238
])
@@ -253,8 +294,8 @@ export const api = {
253294
},
254295

255296
async getTemplate(name: string): Promise<{ template: Template }> {
256-
const data = await graphqlQuery<Infra<{ Template?: { metadata: { name: string }; spec: Record<string, unknown> } }>>(
257-
`query($n: String!) { ${GROUP_FIELD} { ${VERSION} { Template(name: $n) { metadata { name } spec { displayName description category version iconURL backend instanceCRD { group version resource kind } schema } } } } }`,
297+
const data = await templateQuery<Infra<{ Template?: { metadata: { name: string }; spec: Record<string, unknown> } }>>(
298+
spec => `query($n: String!) { ${GROUP_FIELD} { ${VERSION} { Template(name: $n) { metadata { name } spec { ${spec} } } } } }`,
258299
{ n: name },
259300
)
260301
const t = data[GROUP_FIELD]?.[VERSION]?.Template

0 commit comments

Comments
 (0)