Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions apps/console/src/routeTree.gen.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useCallback, useEffect } from 'react'
import { useEnvironment } from '@qovery/domains/environments/feature'
import { BlueprintUpdateFlow, useService } from '@qovery/domains/services/feature'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/update/blueprint'
)({
component: RouteComponent,
})

function RouteComponent() {
const { organizationId, projectId, environmentId, serviceId } = Route.useParams()
const navigate = useNavigate()
const { data: environment } = useEnvironment({ environmentId, suspense: true })
const { data: service } = useService({ environmentId, serviceId, suspense: true })
const blueprintId = service && 'blueprint_id' in service ? service.blueprint_id : undefined
const navigateToOverview = useCallback(() => {
navigate({
to: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/overview',
params: { organizationId, projectId, environmentId, serviceId },
})
}, [environmentId, navigate, organizationId, projectId, serviceId])

useEffect(() => {
if (service && !blueprintId) navigateToOverview()
}, [blueprintId, navigateToOverview, service])

if (!service || !blueprintId) return null

return (
<BlueprintUpdateFlow
blueprintId={blueprintId}
clusterId={environment?.cluster_id}
environmentId={environmentId}
organizationId={organizationId}
service={service}
onExit={navigateToOverview}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const hiddenProgressCardRouteIds: FileRouteTypes['id'][] = [
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/lifecycle-job',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/terraform',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/update/blueprint',
]

function RouteComponent() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ const bypassLayoutRouteIds: FileRouteTypes['id'][] = [
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/terraform',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/lifecycle-job',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/cron-job',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/update/blueprint',
]

function useBypassLayout(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
BlueprintCatalogApi,
type BlueprintCreateRequest,
BlueprintMainCallsApi,
type BlueprintUpdateRequest,
type CleanFailedJobsRequest,
ContainerActionsApi,
type ContainerAdvancedSettings,
Expand Down Expand Up @@ -769,6 +770,16 @@ type CreateBlueprintRequest = {
deploy?: boolean
}

type PreviewBlueprintUpdateRequest = {
blueprintId: string
payload: BlueprintUpdateRequest
}

type UpdateBlueprintRequest = {
blueprintId: string
payload: BlueprintUpdateRequest
}

type EditServiceRequest = {
serviceId: string
payload:
Expand Down Expand Up @@ -1042,6 +1053,14 @@ export const mutations = {
const response = await blueprintApi.createBlueprint(environmentId, payload, deploy)
return response.data
},
async previewBlueprintUpdate({ blueprintId, payload }: PreviewBlueprintUpdateRequest) {
const response = await blueprintApi.previewBlueprintUpdate(blueprintId, payload)
return response.data
},
async updateBlueprint({ blueprintId, payload }: UpdateBlueprintRequest) {
const response = await blueprintApi.updateBlueprint(blueprintId, payload)
return response.data
},
async editService({ serviceId, payload }: EditServiceRequest) {
const { mutation } = match(payload)
.with({ serviceType: 'APPLICATION' }, ({ serviceType, ...payload }) => ({
Expand Down
4 changes: 4 additions & 0 deletions libs/domains/services/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export * from './lib/hooks/use-blueprint-catalog/use-blueprint-catalog'
export * from './lib/hooks/use-blueprint-catalog-service-readme/use-blueprint-catalog-service-readme'
export * from './lib/hooks/use-blueprint-catalog-service-manifest/use-blueprint-catalog-service-manifest'
export * from './lib/hooks/use-blueprint-update/use-blueprint-update'
export * from './lib/hooks/use-blueprint-update-preview-socket/use-blueprint-update-preview-socket'
export * from './lib/hooks/use-preview-blueprint-update/use-preview-blueprint-update'
export * from './lib/hooks/use-update-blueprint/use-update-blueprint'
export * from './lib/hooks/use-blueprint-service-created-socket/use-blueprint-service-created-socket'
export * from './lib/blueprint-query-boundary/blueprint-query-boundary'
export * from './lib/hooks/use-service-deployment-and-running-statuses/use-service-deployment-and-running-statuses'
Expand Down Expand Up @@ -102,6 +105,7 @@ export * from './lib/service-creation-flow/database/step-resources/step-resource
export * from './lib/service-creation-flow/database/database-summary-view/database-summary-view'
export * from './lib/service-creation-flow/database/step-summary/step-summary'
export * from './lib/service-creation-flow/blueprint/blueprint-creation-flow'
export * from './lib/service-update-flow/blueprint/blueprint-update-flow'
export * from './lib/application-container-healthchecks/application-container-healthchecks-form/application-container-healthchecks-form'
export * from './lib/application-container-healthchecks/healthchecks-utils'
export * from './lib/application-container-healthchecks/step-healthchecks/step-healthchecks'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
type BlueprintManifestContextVariableField,
type BlueprintManifestResponseResultsInner,
type BlueprintManifestVariableField,
} from 'qovery-typescript-axios'

export type BlueprintFieldValue = string | boolean
export type BlueprintFieldValues = Record<string, BlueprintFieldValue>
export type BlueprintFieldPath = `fields.${string}`

export type OverridableBlueprintManifestContextVariableField = BlueprintManifestContextVariableField & {
overridable?: boolean
}

export function getBlueprintFieldPath(name: string): BlueprintFieldPath {
return `fields.${name}`
}

export function formatFieldLabel(name: string) {
const label = name.replace(/_/g, ' ')
return `${label.charAt(0).toUpperCase()}${label.slice(1)}`
}

export function getDefaultFieldValue(field: BlueprintManifestVariableField): BlueprintFieldValue {
if (field.type.type === 'bool') return field.default_value === 'true'
return field.default_value ?? ''
}

export function getDefaultContextFieldValue(field: BlueprintManifestContextVariableField): BlueprintFieldValue {
return field.value ?? ''
}

export function getDefaultBlueprintFieldValues(blueprintManifestFields: BlueprintManifestResponseResultsInner[]) {
const requiredBlueprintFields = blueprintManifestFields.filter(isRequiredVariableField)
const optionalBlueprintFields = blueprintManifestFields.filter(isOptionalVariableField)
const overridableContextBlueprintFields = blueprintManifestFields.filter(isOverridableContextVariableField)
const fieldsWithDefaultValue = [...requiredBlueprintFields, ...optionalBlueprintFields]

return {
...Object.fromEntries(fieldsWithDefaultValue.map((field) => [field.name, getDefaultFieldValue(field)])),
...Object.fromEntries(
overridableContextBlueprintFields.map((field) => [field.name, getDefaultContextFieldValue(field)])
),
}
}

export function getStringFieldValue(value: BlueprintFieldValue | undefined) {
return typeof value === 'string' ? value : ''
}

export function getBooleanFieldValue(value: BlueprintFieldValue | undefined) {
return typeof value === 'boolean' ? value : false
}

export function isFieldValueFulfilled(value: BlueprintFieldValue | undefined) {
if (typeof value === 'boolean') return true
return Boolean(value?.trim())
}

export function isFieldValueMatchingPattern(
field: BlueprintManifestVariableField,
value: BlueprintFieldValue | undefined
) {
if (typeof value !== 'string' || !value || !field.type.pattern) return true

try {
return new RegExp(field.type.pattern).test(value)
} catch {
return true
}
}

export function getFieldLengthValidationError(
field: BlueprintManifestVariableField,
value: BlueprintFieldValue | undefined
) {
if (typeof value !== 'string' || !value) return undefined

const { min_length: minLength, max_length: maxLength } = field.type
const hasMinLength = typeof minLength === 'number'
const hasMaxLength = typeof maxLength === 'number'

if (hasMinLength && hasMaxLength && (value.length < minLength || value.length > maxLength)) {
return `Value must be between ${minLength} and ${maxLength} characters.`
}

if (hasMinLength && value.length < minLength) return `Value must be at least ${minLength} characters.`
if (hasMaxLength && value.length > maxLength) return `Value must be at most ${maxLength} characters.`

return undefined
}

export function getFieldValidationError(field: BlueprintManifestVariableField, value: BlueprintFieldValue | undefined) {
const lengthValidationError = getFieldLengthValidationError(field, value)
if (lengthValidationError) return lengthValidationError

if (!isFieldValueMatchingPattern(field, value)) return 'Value does not match the expected format.'
return undefined
}

export function isFieldValid(field: BlueprintManifestVariableField, value: BlueprintFieldValue | undefined) {
if (field.required && !isFieldValueFulfilled(value)) return false
return !getFieldValidationError(field, value)
}

export function isVariableField(field: BlueprintManifestResponseResultsInner): field is BlueprintManifestVariableField {
return field.kind === 'variable'
}

export function isRequiredVariableField(
field: BlueprintManifestResponseResultsInner
): field is BlueprintManifestVariableField {
return isVariableField(field) && field.required
}

export function isOptionalVariableField(
field: BlueprintManifestResponseResultsInner
): field is BlueprintManifestVariableField {
return isVariableField(field) && !field.required
}

export function isOverridableContextVariableField(
field: BlueprintManifestResponseResultsInner
): field is OverridableBlueprintManifestContextVariableField {
return field.kind === 'contextVariable' && 'overridable' in field && field.overridable === true
}

export function getSummaryFieldValue(
field: BlueprintManifestVariableField | OverridableBlueprintManifestContextVariableField,
value: BlueprintFieldValue | undefined
) {
if (typeof value === 'boolean') return value ? 'Enabled' : 'Disabled'
if (field.kind === 'variable' && field.is_secret && value) return '••••••••'
return value
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import {
formatFieldLabel,
getBooleanFieldValue,
getStringFieldValue,
} from '../../../blueprint-creation-utils/blueprint-creation-utils'
import { CheckboxField } from '../checkbox-field/checkbox-field'
} from '../blueprint-field-utils/blueprint-field-utils'
import { CheckboxField } from './checkbox-field'

export interface BlueprintManifestVariableInputProps {
autoFocus?: boolean
error?: string
field: BlueprintManifestVariableField
label?: string
onChange: (value: BlueprintFieldValue) => void
value: BlueprintFieldValue | undefined
}
Expand All @@ -20,15 +21,16 @@ export function BlueprintManifestVariableInput({
autoFocus,
error,
field,
label,
onChange,
value,
}: BlueprintManifestVariableInputProps) {
const label = formatFieldLabel(field.name)
const inputLabel = label ?? formatFieldLabel(field.name)

if (field.allowed_values?.length) {
return (
<InputSelect
label={label}
label={inputLabel}
value={getStringFieldValue(value)}
options={field.allowed_values.map((allowedValue) => ({ label: allowedValue, value: allowedValue }))}
autoFocus={autoFocus}
Expand All @@ -46,7 +48,7 @@ export function BlueprintManifestVariableInput({
autoFocus={autoFocus}
checked={getBooleanFieldValue(value)}
description={field.description ?? ''}
label={label}
label={inputLabel}
name={field.name}
onChange={onChange}
/>
Expand All @@ -56,7 +58,7 @@ export function BlueprintManifestVariableInput({
return (
<InputText
name={field.name}
label={label}
label={inputLabel}
type={field.type.type === 'number' ? 'number' : field.is_secret ? 'password' : 'text'}
value={getStringFieldValue(value)}
error={error}
Expand Down
Loading
Loading