Skip to content
Open
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
2 changes: 2 additions & 0 deletions libs/domains/services/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export * from './lib/hooks/use-recent-services/use-recent-services'
export * from './lib/hooks/use-favorite-services/use-favorite-services'
export * from './lib/pod-statuses-callout/pod-statuses-callout'
export * from './lib/pods-metrics/pods-metrics'
export * from './lib/keda/scaled-object-status/scaled-object-status'
export * from './lib/keda/components'
export * from './lib/service-action-toolbar/service-action-toolbar'
export * from './lib/service-deployment-status-label/service-deployment-status-label'
export * from './lib/service-details/service-details'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { type Control, Controller, type FieldValues, type UseFormSetValue } from 'react-hook-form'
import { InputText, RadioGroup } from '@qovery/shared/ui'

export interface HpaMetricFieldsProps {
control: Control
setValue: UseFormSetValue<FieldValues>
hpaMetricType?: string
}

export function HpaMetricFields({ control, setValue, hpaMetricType }: HpaMetricFieldsProps) {
return (
<>
{/* Metric choice */}
<Controller
name="hpa_metric_type"
control={control}
render={({ field }) => (
<div className="mb-5">
<label className="mb-3 block text-sm font-medium text-neutral-400">Autoscaling metric</label>
<RadioGroup.Root onValueChange={field.onChange} value={field.value} className="flex flex-col gap-3">
<label className="flex cursor-pointer items-start gap-3 rounded border border-neutral-250 bg-neutral-100 p-4">
<RadioGroup.Item value="CPU" />
<div className="flex flex-col gap-1">
<span className="font-medium text-neutral-400">CPU only</span>
<span className="text-sm text-neutral-350">Scale based on CPU usage</span>
</div>
</label>
<label className="flex cursor-pointer items-start gap-3 rounded border border-neutral-250 bg-neutral-100 p-4">
<RadioGroup.Item value="CPU_AND_MEMORY" />
<div className="flex flex-col gap-1">
<span className="font-medium text-neutral-400">CPU + Memory</span>
<span className="text-sm text-neutral-350">Scale based on CPU and memory usage</span>
</div>
</label>
</RadioGroup.Root>
</div>
)}
/>

{/* CPU threshold */}
<Controller
name="hpa_cpu_average_utilization_percent"
control={control}
rules={{
required: true,
min: 1,
max: 100,
}}
render={({ field, fieldState: { error } }) => (
<InputText
type="number"
label="CPU average utilization (%)"
name={field.name}
value={isNaN(field.value) || field.value === 0 ? '' : field.value.toString()}
onChange={(e) => {
const output = parseInt(e.target.value, 10)
const value = isNaN(output) ? 0 : output
field.onChange(value)
}}
error={
error?.type === 'required'
? 'Please enter a value.'
: error?.type === 'min'
? 'Minimum is 1%.'
: error?.type === 'max'
? 'Maximum is 100%.'
: undefined
}
/>
)}
/>

{/* Memory threshold (only if CPU_AND_MEMORY) */}
{hpaMetricType === 'CPU_AND_MEMORY' && (
<Controller
name="hpa_memory_average_utilization_percent"
control={control}
rules={{
required: true,
min: 1,
max: 100,
}}
render={({ field, fieldState: { error } }) => (
<InputText
type="number"
label="Memory average utilization (%)"
name={field.name}
value={isNaN(field.value) || field.value === 0 ? '' : field.value.toString()}
onChange={(e) => {
const output = parseInt(e.target.value, 10)
const value = isNaN(output) ? 0 : output
field.onChange(value)
}}
error={
error?.type === 'required'
? 'Please enter a value.'
: error?.type === 'min'
? 'Minimum is 1%.'
: error?.type === 'max'
? 'Maximum is 100%.'
: undefined
}
/>
)}
/>
)}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './instances-range-inputs'
export * from './hpa-metric-fields'
export * from './keda-scalers-fields'
export * from './keda-settings'
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { FormProvider, useForm } from 'react-hook-form'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { InstancesRangeInputs } from './instances-range-inputs'

function TestWrapper({
requireMinLessThanMax,
showMaxField = true,
}: {
requireMinLessThanMax?: boolean
showMaxField?: boolean
}) {
const methods = useForm({
defaultValues: {
min_running_instances: 1,
max_running_instances: 5,
},
})

return (
<FormProvider {...methods}>
<form>
<InstancesRangeInputs
control={methods.control}
minInstances={1}
maxInstances={100}
minRunningInstances={methods.watch('min_running_instances')}
showMaxField={showMaxField}
requireMinLessThanMax={requireMinLessThanMax}
/>
</form>
</FormProvider>
)
}

describe('InstancesRangeInputs', () => {
it('should render min and max fields', () => {
renderWithProviders(<TestWrapper />)

expect(screen.getByLabelText('Instances min')).toBeInTheDocument()
expect(screen.getByLabelText('Instances max')).toBeInTheDocument()
})

it('should render only min field when showMaxField is false', () => {
renderWithProviders(<TestWrapper showMaxField={false} />)

expect(screen.getByLabelText('Number of instances')).toBeInTheDocument()
expect(screen.queryByLabelText('Instances max')).not.toBeInTheDocument()
})

it('should render with requireMinLessThanMax enabled', () => {
renderWithProviders(<TestWrapper requireMinLessThanMax={true} />)

expect(screen.getByLabelText('Instances min')).toBeInTheDocument()
expect(screen.getByLabelText('Instances max')).toBeInTheDocument()
})

it('should render with requireMinLessThanMax disabled', () => {
renderWithProviders(<TestWrapper requireMinLessThanMax={false} />)

expect(screen.getByLabelText('Instances min')).toBeInTheDocument()
expect(screen.getByLabelText('Instances max')).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { type Control, Controller, type FieldErrors, useWatch } from 'react-hook-form'
import { InputText } from '@qovery/shared/ui'

export interface InstancesRangeInputsProps {
control: Control
minInstances: number
maxInstances: number
minRunningInstances?: number
disabled?: boolean
showMaxField?: boolean
runningPods?: number
requireMinLessThanMax?: boolean
}

export function InstancesRangeInputs({
control,
minInstances,
maxInstances,
minRunningInstances,
disabled = false,
showMaxField = true,
runningPods,
requireMinLessThanMax = false,
}: InstancesRangeInputsProps) {
const minRunningInstancesValue = useWatch({ control, name: 'min_running_instances' })

const getErrorMessage = (error: FieldErrors[string], fieldName: string) => {
if (!error) return undefined
if (error.type === 'required') return 'Please enter a size.'
if (error.type === 'max') return `Maximum allowed is: ${maxInstances}.`
if (error.type === 'min') {
const minValue = fieldName === 'max_running_instances' ? minRunningInstancesValue : minInstances
return `Minimum allowed is: ${minValue}.`
}
if (error.type === 'validate') return error.message as string
return undefined
}

return (
<>
<div className={showMaxField ? 'grid grid-cols-2 gap-4' : ''}>
<Controller
name="min_running_instances"
control={control}
rules={{
required: true,
min: minInstances,
max: maxInstances,
}}
render={({ field, fieldState: { error } }) => (
<InputText
type="number"
label={showMaxField ? 'Instances min' : 'Number of instances'}
name={field.name}
value={isNaN(field.value) || field.value === 0 ? '' : field.value.toString()}
onChange={(e) => {
const output = parseInt(e.target.value, 10)
const value = isNaN(output) ? 0 : output
field.onChange(value)
}}
disabled={disabled}
error={getErrorMessage(error, field.name)}
/>
)}
/>

{showMaxField && (
<Controller
name="max_running_instances"
control={control}
rules={{
required: true,
max: maxInstances,
min: minRunningInstancesValue,
validate: requireMinLessThanMax
? (value: number) => {
if (value <= minRunningInstancesValue) {
return 'Maximum instances must be greater than minimum instances.'
}
return true
}
: undefined,
}}
render={({ field, fieldState: { error } }) => (
<InputText
type="number"
label="Instances max"
name={field.name}
value={isNaN(field.value) || field.value === 0 ? '' : field.value.toString()}
onChange={(e) => {
const output = parseInt(e.target.value, 10)
const value = isNaN(output) ? 0 : output
field.onChange(value)
}}
disabled={disabled}
error={getErrorMessage(error, field.name)}
/>
)}
/>
)}
</div>

{runningPods !== undefined && (
<p className="text-xs text-neutral-350">
<span className="mb-1 flex">
Current consumption: {runningPods} instance{runningPods > 1 ? 's' : ''}
</span>
</p>
)}
</>
)
}
Loading