diff --git a/libs/domains/clusters/feature/src/index.ts b/libs/domains/clusters/feature/src/index.ts index b22163cacbe..0ed72c71483 100644 --- a/libs/domains/clusters/feature/src/index.ts +++ b/libs/domains/clusters/feature/src/index.ts @@ -5,6 +5,7 @@ export * from './lib/cluster-action-toolbar/cluster-action-toolbar' export * from './lib/cluster-avatar/cluster-avatar' export * from './lib/cluster-setup/cluster-setup' export * from './lib/cluster-card/cluster-card' +export * from './lib/nodepools-resources-settings/nodepools-resources-settings' export * from './lib/kubeconfig-preview/kubeconfig-preview' export * from './lib/hooks/use-cluster-cloud-provider-info/use-cluster-cloud-provider-info' export * from './lib/hooks/use-cluster-routing-table/use-cluster-routing-table' diff --git a/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.spec.tsx b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.spec.tsx new file mode 100644 index 00000000000..ec5b1596496 --- /dev/null +++ b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.spec.tsx @@ -0,0 +1,121 @@ +import { type Cluster } from 'qovery-typescript-axios' +import selectEvent from 'react-select-event' +import { fireEvent, renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests' +import { NodepoolModal } from './nodepool-modal' + +const mockCluster = { + region: 'us-east-1', +} + +const defaultProps = { + type: 'stable' as const, + cluster: mockCluster as Cluster, + onChange: jest.fn(), +} + +describe('NodepoolModal', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should render correctly for stable type', () => { + renderWithProviders() + + expect(screen.getByText('Nodepool stable')).toBeInTheDocument() + expect(screen.getByLabelText('vCPU')).toBeInTheDocument() + expect(screen.getByLabelText('Memory (GiB)')).toBeInTheDocument() + expect(screen.getByText('Consolidation schedule')).toBeInTheDocument() + }) + + it('should render correctly for default type', () => { + renderWithProviders() + + expect(screen.getByText('Nodepool stable')).toBeInTheDocument() + expect(screen.getByLabelText('vCPU')).toBeInTheDocument() + expect(screen.getByLabelText('Memory (GiB)')).toBeInTheDocument() + expect(screen.getByText('Operates every day, 24 hours a day')).toBeInTheDocument() + }) + + it('should validate minimum values for CPU and Memory', async () => { + const { userEvent } = renderWithProviders() + + const cpuInput = screen.getByLabelText('vCPU') + const memoryInput = screen.getByLabelText('Memory (GiB)') + + await userEvent.clear(cpuInput) + await userEvent.type(cpuInput, '2') + await userEvent.clear(memoryInput) + await userEvent.type(memoryInput, '5') + + const submitButton = screen.getByText('Confirm') + await userEvent.click(submitButton) + + expect(screen.getByText('Minimum allowed is: 6 milli vCPU.')).toBeInTheDocument() + expect(screen.getByText('Minimum allowed is: 10 GiB.')).toBeInTheDocument() + }) + + it('should show consolidation fields when enabled for stable type', async () => { + const { userEvent } = renderWithProviders() + + const consolidationToggle = screen.getByText('Consolidation schedule') + await userEvent.click(consolidationToggle) + + expect(screen.getByLabelText(/Start time/)).toBeInTheDocument() + expect(screen.getByLabelText('Duration')).toBeInTheDocument() + expect(screen.getByLabelText('Days')).toBeInTheDocument() + }) + + it('should submit form with correct values', async () => { + const onChangeMock = jest.fn() + const { userEvent } = renderWithProviders() + + const cpuInput = screen.getByLabelText('vCPU') + const memoryInput = screen.getByLabelText('Memory (GiB)') + + await userEvent.clear(cpuInput) + await userEvent.type(cpuInput, '8') + await userEvent.clear(memoryInput) + await userEvent.type(memoryInput, '16') + + const consolidationToggle = screen.getByText('Consolidation schedule') + await userEvent.click(consolidationToggle) + + const startTimeInput = screen.getByLabelText(/Start time/) + + // XXX: fireEvent is necessary here because userEvent.type is not working properly with time inputs + // `showPicker()` in our input provides error + fireEvent.change(startTimeInput, { target: { value: '21:00' } }) + + const durationInput = screen.getByLabelText('Duration') + await userEvent.type(durationInput, '8h00m') + + const daysSelect = screen.getByLabelText('Days') + await selectEvent.select(daysSelect, 'Monday', { + container: document.body, + }) + + const submitButton = screen.getByText('Confirm') + await waitFor(() => { + expect(submitButton).toBeEnabled() + }) + + await userEvent.click(submitButton) + + await waitFor(() => { + expect(onChangeMock).toHaveBeenCalledWith({ + stable_override: { + limits: { + max_cpu_in_vcpu: '8', + max_memory_in_gibibytes: '16', + }, + consolidation: { + enabled: true, + days: ['MONDAY'], + start_time: 'PT21:00', + duration: 'PT8H00M', + }, + }, + }) + }) + }) +}) diff --git a/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx new file mode 100644 index 00000000000..7585e32cf51 --- /dev/null +++ b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx @@ -0,0 +1,292 @@ +import { + type Cluster, + type KarpenterDefaultNodePoolOverride, + type KarpenterNodePool, + type KarpenterStableNodePoolOverride, + WeekdayEnum, +} from 'qovery-typescript-axios' +import { Controller, FormProvider, useForm, useFormContext } from 'react-hook-form' +import { P, match } from 'ts-pattern' +import { Callout, Icon, InputSelect, InputText, InputToggle, ModalCrud, Tooltip, useModal } from '@qovery/shared/ui' +import { upperCaseFirstLetter } from '@qovery/shared/util-js' + +function LimitsFields({ type }: { type: 'default' | 'stable' }) { + const { control } = useFormContext() + + const name = `${type === 'default' ? 'default_override' : 'stable_override'}.limits` + + return ( + <> + ( + + )} + /> + ( + + )} + /> + + ) +} + +export interface NodepoolModalProps { + type: 'stable' | 'default' + cluster: Cluster + onChange: (data: Omit) => void + defaultValues?: KarpenterStableNodePoolOverride | KarpenterDefaultNodePoolOverride +} + +const CPU_MIN = 6 +const MEMORY_MIN = 10 + +export function NodepoolModal({ type, cluster, onChange, defaultValues }: NodepoolModalProps) { + const { closeModal } = useModal() + + const methods = useForm>({ + mode: 'onChange', + defaultValues: { + default_override: { + limits: defaultValues?.limits, + }, + stable_override: { + ...defaultValues, + ...{ + consolidation: match(defaultValues) + .with({ consolidation: P.not(P.nullish) }, ({ consolidation }) => ({ + ...consolidation, + start_time: consolidation.start_time.replace('PT', ''), + duration: consolidation.duration.replace('PT', ''), + })) + .otherwise(() => ({ + start_time: '', + duration: '', + })), + }, + }, + }, + }) + + const watchConsolidation = methods.watch('stable_override.consolidation.enabled') + + const onSubmit = methods.handleSubmit(async (data) => { + onChange({ + ...(type === 'default' + ? { + default_override: { + limits: { + max_cpu_in_vcpu: data.default_override?.limits?.max_cpu_in_vcpu ?? CPU_MIN, + max_memory_in_gibibytes: data.default_override?.limits?.max_memory_in_gibibytes ?? MEMORY_MIN, + }, + }, + } + : { + stable_override: { + limits: { + max_cpu_in_vcpu: data.stable_override?.limits?.max_cpu_in_vcpu ?? CPU_MIN, + max_memory_in_gibibytes: data.stable_override?.limits?.max_memory_in_gibibytes ?? MEMORY_MIN, + }, + consolidation: { + enabled: data.stable_override?.consolidation?.enabled ?? false, + days: data.stable_override?.consolidation?.days ?? [], + start_time: data.stable_override?.consolidation?.start_time + ? `PT${data.stable_override.consolidation.start_time}` + : '', + duration: data.stable_override?.consolidation?.duration + ? `PT${data.stable_override.consolidation.duration.toUpperCase()}` + : '', + }, + }, + }), + }) + + closeModal() + }) + + const daysOptions = Object.keys(WeekdayEnum).map((key) => ({ + label: upperCaseFirstLetter(key), + value: key, + })) + + return ( + + +
+
+
+

Nodepool resources limits

+

Limit resources to control usage and avoid unexpected costs.

+
+ + + + + +
+ +
+
+ {type === 'default' && ( +
+ + + + + +
+

Operates every day, 24 hours a day

+ + Define when consolidation occurs to optimize resource usage by reducing the number of active nodes. + +
+
+ )} + {type === 'stable' && ( + <> + ( +
+ + + + + + +
+ )} + /> + {watchConsolidation && ( +
+ + Some downtime may occur during this process. + + ( + + )} + /> + { + const match = value.match(/^(\d{1,2})[hH](\d{1,2})[mM]$/) + if (!match) return "Invalid format. Use '2h10m' or '2H10M'." + + const hours = parseInt(match[1], 10) + const minutes = parseInt(match[2], 10) + const totalMinutes = hours * 60 + minutes + + if (hours >= 24 || minutes >= 60) { + return 'Hours must be less than 24 and minutes less than 60.' + } + if (totalMinutes >= 1440) { + return 'Duration must be less than 24 hours (1440 minutes).' + } + return true + }, + }} + render={({ field, fieldState: { error } }) => ( + + )} + /> + ( + + )} + /> +
+ )} + + )} +
+
+
+ ) +} + +export default NodepoolModal diff --git a/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.spec.tsx b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.spec.tsx new file mode 100644 index 00000000000..aa9a7a5658e --- /dev/null +++ b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.spec.tsx @@ -0,0 +1,130 @@ +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { type Cluster, WeekdayEnum } from 'qovery-typescript-axios' +import { renderWithProviders, screen } from '@qovery/shared/util-tests' +import { NodepoolsResourcesSettings, formatTimeRange, formatWeekdays, shortenDay } from './nodepools-resources-settings' + +const mockCluster = { + features: [ + { + id: 'KARPENTER', + value_object: { + value: { + qovery_node_pools: { + default_override: { + limits: { + max_cpu_in_vcpu: 8, + max_memory_in_gibibytes: 16, + }, + }, + stable_override: { + limits: { + max_cpu_in_vcpu: 8, + max_memory_in_gibibytes: 16, + }, + consolidation: { + enabled: true, + days: ['MONDAY'], + start_time: 'PT22:00', + duration: 'PT8H', + }, + }, + }, + }, + }, + }, + ], + region: 'us-east-1', +} as Cluster + +describe('NodepoolsResourcesSettings', () => { + describe('formatTimeRange', () => { + it('should format time range correctly', () => { + expect(formatTimeRange('PT22:00', 'PT8H')).toEqual({ + start: '10:00 pm', + end: '6:00 am', + }) + }) + + it('should handle hours and minutes in duration', () => { + expect(formatTimeRange('PT22:00', 'PT1H30M')).toEqual({ + start: '10:00 pm', + end: '11:30 pm', + }) + }) + }) + + describe('shortenDay', () => { + it('should shorten day names correctly', () => { + expect(shortenDay('MONDAY')).toBe('Mon') + expect(shortenDay('FRIDAY')).toBe('Fri') + }) + }) + + describe('formatWeekdays', () => { + it('should handle empty array', () => { + expect(formatWeekdays([])).toBe('') + }) + + it('should return "Operates every day" for full week', () => { + const fullWeek = Object.keys(WeekdayEnum) + expect(formatWeekdays(fullWeek)).toBe('Operates every day') + }) + + it('should format consecutive days with "to"', () => { + expect(formatWeekdays(['MONDAY', 'TUESDAY', 'WEDNESDAY'])).toBe('Monday to Wednesday') + }) + + it('should format single day', () => { + expect(formatWeekdays(['MONDAY'])).toBe('Monday') + }) + + it('should format non-consecutive days with commas', () => { + expect(formatWeekdays(['MONDAY', 'WEDNESDAY', 'FRIDAY'])).toBe('Mon, Wed, Fri') + }) + }) + + describe('Component', () => { + it('should display default values from cluster configuration', () => { + const { debug, baseElement } = renderWithProviders( + wrapWithReactHookForm(, { + defaultValues: { + karpenter: { + qovery_node_pools: { + default_override: { + limits: { + max_cpu_in_vcpu: 12, + max_memory_in_gibibytes: 24, + }, + }, + stable_override: { + limits: { + max_cpu_in_vcpu: 16, + max_memory_in_gibibytes: 32, + }, + consolidation: { + enabled: true, + days: ['MONDAY', 'WEDNESDAY', 'FRIDAY'], + start_time: 'PT20:00', + duration: 'PT4H', + }, + }, + }, + }, + }, + }) + ) + + debug(baseElement, 10000) + + // Check stable nodepool values + expect(screen.getByText('vCPU limit: 16 vCPU;')).toBeInTheDocument() + expect(screen.getByText('Memory list: 32 GiB')).toBeInTheDocument() + expect(screen.getByText('Mon, Wed, Fri,')).toBeInTheDocument() + expect(screen.getByText('8:00 pm to 12:00 am')).toBeInTheDocument() + + // Check default nodepool values + expect(screen.getByText('vCPU limit: 12 vCPU;')).toBeInTheDocument() + expect(screen.getByText('Memory list: 24 GiB')).toBeInTheDocument() + }) + }) +}) diff --git a/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.tsx b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.tsx new file mode 100644 index 00000000000..f338d57c348 --- /dev/null +++ b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.tsx @@ -0,0 +1,211 @@ +import { add, format, parse } from 'date-fns' +import { type Cluster, WeekdayEnum } from 'qovery-typescript-axios' +import { useFormContext } from 'react-hook-form' +import { type ClusterResourcesData } from '@qovery/shared/interfaces' +import { BlockContent, Button, Icon, Tooltip, useModal } from '@qovery/shared/ui' +import { upperCaseFirstLetter } from '@qovery/shared/util-js' +import { NodepoolModal } from './nodepool-modal/nodepool-modal' + +export const formatTimeRange = ( + startTime?: string, + duration?: string +): { + start: string + end: string +} => { + if (startTime === undefined || duration === undefined || startTime === '' || duration === '') + return { start: '', end: '' } + + const baseDate = parse(startTime.replace('PT', ''), 'HH:mm', new Date()) + + const durationHours = parseInt(duration.match(/(\d+)H/)?.[1] || '0') + const durationMinutes = parseInt(duration.match(/(\d+)M/)?.[1] || '0') + + const endDate = add(baseDate, { + hours: durationHours, + minutes: durationMinutes, + }) + + return { + start: format(baseDate, 'h:mm a').toLowerCase(), + end: format(endDate, 'h:mm a').toLowerCase(), + } +} + +export const shortenDay = (day: string): string => { + return upperCaseFirstLetter(day).slice(0, 3) +} + +export const formatWeekdays = (days: string[]): string => { + if (days.length === 0) return '' + + const fullWeek = days.length === 7 + if (fullWeek) { + return 'Operates every day' + } + + const weekdayOrder = Object.keys(WeekdayEnum).map((day) => day) + const daysIndices = days.map((day) => weekdayOrder.indexOf(day)).sort((a, b) => a - b) + + const isConsecutive = daysIndices.every((day, index) => { + if (index === 0) return true + return day === daysIndices[index - 1] + 1 + }) + + if (isConsecutive) { + if (days.length > 1) { + return `${upperCaseFirstLetter(days[0])} to ${upperCaseFirstLetter(days[days.length - 1])}` + } else { + return upperCaseFirstLetter(days[0]) + } + } + + return days.map(shortenDay).join(', ') +} + +export interface NodepoolsResourcesSettingsProps { + cluster: Cluster +} + +export function NodepoolsResourcesSettings({ cluster }: NodepoolsResourcesSettingsProps) { + const { openModal } = useModal() + const { watch, setValue } = useFormContext() + + const watchStable = watch('karpenter.qovery_node_pools.stable_override') + const watchDefault = watch('karpenter.qovery_node_pools.default_override') + + const { start, end } = formatTimeRange(watchStable?.consolidation?.start_time, watchStable?.consolidation?.duration) + + return ( + + + + + + } + > +
+
+

Stable nodepool

+ + Used for single instances and internal Qovery applications, such as containerized databases, to maintain + stability. + +
+
+
+ {watchStable?.consolidation?.enabled ? ( + + + {formatWeekdays(watchStable?.consolidation?.days)}, + + + + + + + + {start} to {end} + + + ) : ( + Consolidation schedule: disabled + )} + {watchStable?.limits && ( + + {watchStable.limits.max_cpu_in_vcpu && ( + vCPU limit: {watchStable?.limits?.max_cpu_in_vcpu} vCPU; + )} + {watchStable.limits.max_memory_in_gibibytes && ( + Memory list: {watchStable?.limits?.max_memory_in_gibibytes} GiB + )} + + )} +
+ +
+
+
+
+

Default nodepool

+ + Designed to handle general workloads and serves as the foundation for deploying most applications. + +
+
+
+ + + Operates every day, + + + + + + + 24 hours a day + + + {watchDefault?.limits?.max_cpu_in_vcpu && ( + vCPU limit: {watchDefault?.limits?.max_cpu_in_vcpu} vCPU; + )} + {watchDefault?.limits?.max_memory_in_gibibytes && ( + Memory list: {watchDefault?.limits?.max_memory_in_gibibytes} GiB + )} + +
+ +
+
+
+ ) +} + +export default NodepoolsResourcesSettings diff --git a/libs/domains/environments/feature/src/lib/terraform-export-modal/__snapshots__/terraform-export-modal.spec.tsx.snap b/libs/domains/environments/feature/src/lib/terraform-export-modal/__snapshots__/terraform-export-modal.spec.tsx.snap index 58fa7819b53..e11a13dd79c 100644 --- a/libs/domains/environments/feature/src/lib/terraform-export-modal/__snapshots__/terraform-export-modal.spec.tsx.snap +++ b/libs/domains/environments/feature/src/lib/terraform-export-modal/__snapshots__/terraform-export-modal.spec.tsx.snap @@ -44,7 +44,7 @@ exports[`TerraformExportModal should match with snapshots 1`] = `

{ setResourcesData: mockSetResourceData, resourcesData: { instance_type: 't2.medium', - disk_size: 50, cluster_type: 'MANAGED', nodes: [1, 3], karpenter: { @@ -84,28 +83,24 @@ describe('StepResourcesFeature', () => { }) it('should submit form and navigate', async () => { - const { baseElement } = render( + renderWithProviders( ) - const select = getByLabelText(baseElement, 'Instance type') + const select = screen.getByLabelText('Instance type') await selectEvent.select(select, 't2.small (1CPU - 2GB RAM - arm64)', { container: document.body, }) - const diskSize = getByLabelText(baseElement, 'Disk size (GB)') - fireEvent.input(diskSize, { target: { value: '22' } }) - - const button = getByTestId(baseElement, 'button-submit') + const button = screen.getByTestId('button-submit') button.click() await waitFor(() => { expect(button).toBeEnabled() expect(mockSetResourceData).toHaveBeenCalledWith({ instance_type: 't2.small', - disk_size: '22', cluster_type: 'MANAGED', nodes: [1, 3], karpenter: { diff --git a/libs/pages/clusters/src/lib/ui/page-clusters-create/step-summary/step-summary.tsx b/libs/pages/clusters/src/lib/ui/page-clusters-create/step-summary/step-summary.tsx index 610c00c2b4a..2edf9187b01 100644 --- a/libs/pages/clusters/src/lib/ui/page-clusters-create/step-summary/step-summary.tsx +++ b/libs/pages/clusters/src/lib/ui/page-clusters-create/step-summary/step-summary.tsx @@ -161,9 +161,6 @@ export function StepSummary(props: StepSummaryProps) { {props.detailInstanceType?.name} ({props.detailInstanceType?.cpu}CPU -{' '} {props.detailInstanceType?.ram_in_gb}GB RAM - {props.detailInstanceType?.architecture}) -

  • - Disk size: {props.resourcesData.disk_size} GB -
  • Nodes: {props.resourcesData.nodes[0]} min - {props.resourcesData.nodes[1]} max @@ -174,10 +171,6 @@ export function StepSummary(props: StepSummaryProps) {
  • Karpenter: true
  • -
  • - Storage: - {props.resourcesData.karpenter?.disk_size_in_gib} GB -
  • { { label: 'Managed K8S (EKS)', value: 'MANAGED', - }, - { - label: 'BETA - Single EC2 (K3S)', - value: 'SINGLE', + description: 'Multiple node cluster', }, ], - fromDetail: false, + fromDetail: true, cloudProvider: CloudProviderEnum.AWS, } }) @@ -87,13 +84,12 @@ describe('ClusterResourcesSettings', () => { it('should render 2 radios, 1 select, 1 input and 1 slider', () => { renderWithProviders( - wrapWithReactHookForm(, { + wrapWithReactHookForm(, { defaultValues, }) ) - screen.getByLabelText('Managed K8S (EKS)') - screen.getByLabelText('BETA - Single EC2 (K3S)') + screen.getByText('Managed K8S (EKS) - Multiple node cluster') screen.getByLabelText('Instance type') screen.getByLabelText('Disk size (GB)') screen.getByTestId('input-slider') @@ -111,7 +107,7 @@ describe('ClusterResourcesSettings', () => { it('should display banner box', () => { renderWithProviders( - wrapWithReactHookForm(, { + wrapWithReactHookForm(, { defaultValues, }) ) diff --git a/libs/shared/console-shared/src/lib/cluster-settings/ui/cluster-resources-settings/cluster-resources-settings.tsx b/libs/shared/console-shared/src/lib/cluster-settings/ui/cluster-resources-settings/cluster-resources-settings.tsx index aac39dcaed6..d5f99c422b8 100644 --- a/libs/shared/console-shared/src/lib/cluster-settings/ui/cluster-resources-settings/cluster-resources-settings.tsx +++ b/libs/shared/console-shared/src/lib/cluster-settings/ui/cluster-resources-settings/cluster-resources-settings.tsx @@ -1,6 +1,6 @@ import { AnimatePresence, motion } from 'framer-motion' import { CloudProviderEnum, type Cluster, type CpuArchitectureEnum, KubernetesEnum } from 'qovery-typescript-axios' -import { Fragment, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { match } from 'ts-pattern' import { @@ -10,6 +10,7 @@ import { useCloudProviderInstanceTypes, useCloudProviderInstanceTypesKarpenter, } from '@qovery/domains/cloud-providers/feature' +import { NodepoolsResourcesSettings } from '@qovery/domains/clusters/feature' import { IconEnum } from '@qovery/shared/enums' import { type ClusterResourcesData, type Value } from '@qovery/shared/interfaces' import { @@ -299,14 +300,36 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { )} />

    + {props.fromDetail && ( +
    + ( + + )} + /> +
    + )} @@ -318,73 +341,51 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { )} -
    - Resources configuration + {watchKarpenterEnabled && props.cluster && } - {watchKarpenterEnabled ? ( - - ( - + Resources configuration + ( +
    + { + field.onChange(event) + if (props.fromDetail) { + setWarningClusterNodes(true) + } + }} value={field.value} - hint="Storage allocated to your Kubernetes nodes to store files, application images etc.." + label="Instance type" + error={error?.message} + options={instanceTypeOptions} /> - )} - /> - - ) : ( - <> - ( -
    - { - field.onChange(event) - if (props.fromDetail) { - setWarningClusterNodes(true) - } - }} - value={field.value} - label="Instance type" - error={error?.message} - options={instanceTypeOptions} - /> -

    - Instance type to be used to run your Kubernetes nodes. -

    - {warningInstance && ( - - - - - - Be careful - - You selected an instance with ARM64/AARCH64 Cpu architecture. To deploy your services, be sure - all containers and dockerfile you are using are compatible with this CPU architecture - - - - )} -
    - )} - /> +

    Instance type to be used to run your Kubernetes nodes.

    + {warningInstance && ( + + + + + + Be careful + + You selected an instance with ARM64/AARCH64 Cpu architecture. To deploy your services, be sure + all containers and dockerfile you are using are compatible with this CPU architecture + + + + )} +
    + )} + /> + {props.fromDetail && ( )} /> - {warningClusterNodes && ( - - - - - - - Changing these parameters might cause a downtime on your service. - - - - )} - {watchClusterType === KubernetesEnum.MANAGED && ( - <> - Nodes auto-scaling - ( -
    - {watchNodes && ( -

    {`min ${watchNodes[0]} - max ${watchNodes[1]}`}

    - )} - -

    - Cluster can scale up to “max” nodes depending on its usage -

    -
    - )} - /> - - )} - - )} -
    + )} + {warningClusterNodes && ( + + + + + + + Changing these parameters might cause a downtime on your service. + + + + )} + {watchClusterType === KubernetesEnum.MANAGED && ( + <> + Nodes auto-scaling + ( +
    + {watchNodes && ( +

    {`min ${watchNodes[0]} - max ${watchNodes[1]}`}

    + )} + +

    + Cluster can scale up to “max” nodes depending on its usage +

    +
    + )} + /> + + )} + + )} {!props.fromDetail && props.cloudProvider === CloudProviderEnum.AWS && ( diff --git a/libs/shared/factories/src/lib/database-status.mock.ts b/libs/shared/factories/src/lib/database-status.mock.ts index 03602337296..a98e666b630 100644 --- a/libs/shared/factories/src/lib/database-status.mock.ts +++ b/libs/shared/factories/src/lib/database-status.mock.ts @@ -7,6 +7,10 @@ export const databaseStatusFactoryMock = (howMany: number): Status[] => Array.from({ length: howMany }).map((_, index) => ({ id: `${index}`, message: chance.sentence(), + status_details: { + action: 'DEPLOY', + status: 'SUCCESS', + }, service_deployment_status: chance.pickone( Object.values([ ServiceDeploymentStatusEnum.UP_TO_DATE, diff --git a/libs/shared/ui/src/lib/components/inputs/input-toggle/input-toggle.tsx b/libs/shared/ui/src/lib/components/inputs/input-toggle/input-toggle.tsx index 01d43e1e5c7..cc969c61cf2 100644 --- a/libs/shared/ui/src/lib/components/inputs/input-toggle/input-toggle.tsx +++ b/libs/shared/ui/src/lib/components/inputs/input-toggle/input-toggle.tsx @@ -91,7 +91,7 @@ export function InputToggle(props: InputToggleProps) { {title && (
    {title &&

    {title}

    } {description &&
    {description}
    } diff --git a/libs/shared/ui/src/lib/components/tooltip/tooltip.tsx b/libs/shared/ui/src/lib/components/tooltip/tooltip.tsx index 9ca947bbd67..7c1b74779ef 100644 --- a/libs/shared/ui/src/lib/components/tooltip/tooltip.tsx +++ b/libs/shared/ui/src/lib/components/tooltip/tooltip.tsx @@ -3,7 +3,7 @@ import { type VariantProps, cva } from 'class-variance-authority' import { type ComponentProps, type ElementRef, type ReactNode, forwardRef } from 'react' import { twMerge } from '@qovery/shared/util-js' -const tooltipContentVariants = cva(['rounded-sm', 'px-2', 'py-1', 'text-xs', 'font-medium'], { +const tooltipContentVariants = cva(['rounded', 'px-2', 'py-1.5', 'text-xs', 'font-medium'], { variants: { color: { neutral: ['bg-neutral-600', 'text-neutral-50', 'dark:bg-neutral-500'], diff --git a/libs/shared/ui/src/lib/styles/components/input.scss b/libs/shared/ui/src/lib/styles/components/input.scss index c97c171716a..0ded903daa8 100644 --- a/libs/shared/ui/src/lib/styles/components/input.scss +++ b/libs/shared/ui/src/lib/styles/components/input.scss @@ -36,6 +36,7 @@ .input__value[type='time'] { overflow: visible; + color: transparent; } .dark { @@ -57,6 +58,9 @@ label { @apply translate-y-0 text-xs; } + .input__value[type='time'] { + color: theme('colors.neutral.400'); + } } .input--filter { diff --git a/package.json b/package.json index e225d048fa6..3df55c80f14 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "jwt-decode": "^4.0.0", "monaco-editor": "^0.44.0", "posthog-js": "^1.131.4", - "qovery-typescript-axios": "^1.1.517", + "qovery-typescript-axios": "^1.1.526", "react": "18.3.1", "react-country-flag": "^3.0.2", "react-datepicker": "^4.12.0", diff --git a/yarn.lock b/yarn.lock index f947d1acff5..6011a1d2295 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4124,7 +4124,7 @@ __metadata: prettier: ^3.2.5 prettier-plugin-tailwindcss: ^0.5.14 pretty-quick: ^4.0.0 - qovery-typescript-axios: ^1.1.517 + qovery-typescript-axios: ^1.1.526 qovery-ws-typescript-axios: ^0.1.153 react: 18.3.1 react-country-flag: ^3.0.2 @@ -19426,12 +19426,12 @@ __metadata: languageName: node linkType: hard -"qovery-typescript-axios@npm:^1.1.517": - version: 1.1.517 - resolution: "qovery-typescript-axios@npm:1.1.517" +"qovery-typescript-axios@npm:^1.1.526": + version: 1.1.526 + resolution: "qovery-typescript-axios@npm:1.1.526" dependencies: axios: ^0.27.2 - checksum: 14ba9516264a08333493cdd43fb9dffeb474c49acf54c67d5e5d3cc4603f897fb40b186248339f22851c78e88e744afb8dc659cd6c3210d6b30a7ec1a3e14382 + checksum: e192b65682d297b1a9ab41e2da3203335aa430711a3d09f1a830cd3b91c65f508f0be427d042a53e0f2768856e27da59741edc23580417c42652da7f9e3846bd languageName: node linkType: hard