From c9dff5edcf9d0c3713f94fc0d71160b8f9d3ea83 Mon Sep 17 00:00:00 2001 From: Remi Bonnet Date: Tue, 7 Jan 2025 16:58:27 +0100 Subject: [PATCH 01/17] Hide disk size input in the creation flow --- .../step-resources-feature.spec.tsx | 13 +- .../step-summary/step-summary.tsx | 7 - .../cluster-resources-settings.spec.tsx | 14 +- .../cluster-resources-settings.tsx | 210 +++++++++--------- 4 files changed, 113 insertions(+), 131 deletions(-) diff --git a/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/step-resources-feature/step-resources-feature.spec.tsx b/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/step-resources-feature/step-resources-feature.spec.tsx index be95133fa6e..5e648b0b9ed 100644 --- a/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/step-resources-feature/step-resources-feature.spec.tsx +++ b/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/step-resources-feature/step-resources-feature.spec.tsx @@ -1,8 +1,8 @@ -import { fireEvent, getByLabelText, getByTestId, render, waitFor } from '__tests__/utils/setup-jest' import { CloudProviderEnum } from 'qovery-typescript-axios' import { type ReactNode } from 'react' import selectEvent from 'react-select-event' import * as cloudProvidersDomain from '@qovery/domains/cloud-providers/feature' +import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests' import { ClusterContainerCreateContext } from '../page-clusters-create-feature' import StepResourcesFeature from './step-resources-feature' @@ -57,7 +57,6 @@ const ContextWrapper = (props: { children: ReactNode }) => { 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..b46f5dbd23c 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 @@ -299,14 +299,36 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { )} /> + {props.fromDetail && ( +
    + ( + + )} + /> +
    + )} @@ -318,73 +340,49 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { )} -
    - Resources configuration - - {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 && ( From 322b9af4a67dec9ea71860551fb79c983a3ec0e7 Mon Sep 17 00:00:00 2001 From: Remi Bonnet Date: Tue, 7 Jan 2025 17:22:14 +0100 Subject: [PATCH 02/17] Update tooltip rounded and padding --- libs/shared/ui/src/lib/components/tooltip/tooltip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'], From e621a51bdd7ccea7b467808c01a5d99cf9337d1c Mon Sep 17 00:00:00 2001 From: Remi Bonnet Date: Wed, 8 Jan 2025 10:45:28 +0100 Subject: [PATCH 03/17] Update qovery-typescript-axios package --- .../page-clusters-create-feature.tsx | 3 +++ .../step-general-feature/step-general-feature.tsx | 3 +++ libs/shared/factories/src/lib/database-status.mock.ts | 4 ++++ package.json | 2 +- yarn.lock | 10 +++++----- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/page-clusters-create-feature.tsx b/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/page-clusters-create-feature.tsx index d3b3e7d08fd..94e1f6a5666 100644 --- a/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/page-clusters-create-feature.tsx +++ b/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/page-clusters-create-feature.tsx @@ -96,6 +96,9 @@ export const defaultResourcesData: ClusterResourcesData = { default_service_architecture: 'AMD64', disk_size_in_gib: 50, spot_enabled: false, + qovery_node_pools: { + requirements: [], + }, }, } diff --git a/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/step-general-feature/step-general-feature.tsx b/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/step-general-feature/step-general-feature.tsx index ec112e2c847..613588c53b2 100644 --- a/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/step-general-feature/step-general-feature.tsx +++ b/libs/pages/clusters/src/lib/feature/page-clusters-create-feature/step-general-feature/step-general-feature.tsx @@ -47,6 +47,9 @@ export function StepGeneralFeature() { default_service_architecture: 'AMD64', disk_size_in_gib: 50, spot_enabled: false, + qovery_node_pools: { + requirements: [], + }, }, })) } 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/package.json b/package.json index e225d048fa6..e5f6991e81e 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.524", "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..30f9fe25973 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.524 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.524": + version: 1.1.524 + resolution: "qovery-typescript-axios@npm:1.1.524" dependencies: axios: ^0.27.2 - checksum: 14ba9516264a08333493cdd43fb9dffeb474c49acf54c67d5e5d3cc4603f897fb40b186248339f22851c78e88e744afb8dc659cd6c3210d6b30a7ec1a3e14382 + checksum: ea4b2ffb9ba7d26d7cc309253d6ebeb36ce6898096aa97975c0fbcd48a277ae859aa38c22fdfabb028a2e243e4916c43cecb99d17cdd5d0cce5c9ed1afd9d0c2 languageName: node linkType: hard From 42e3802a512a9c4a46369960091a898958e7bdb5 Mon Sep 17 00:00:00 2001 From: Remi Bonnet Date: Thu, 9 Jan 2025 18:33:13 +0100 Subject: [PATCH 04/17] Add modal stable and default for nodepools --- libs/domains/clusters/feature/src/index.ts | 1 + .../nodepool-modal/nodepool-modal.spec.tsx | 0 .../nodepool-modal/nodepool-modal.tsx | 254 ++++++++++++++++++ .../nodepools-resources-settings.spec.tsx | 0 .../nodepools-resources-settings.tsx | 76 ++++++ .../cluster-resources-settings.tsx | 5 +- .../inputs/input-toggle/input-toggle.tsx | 2 +- 7 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.spec.tsx create mode 100644 libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx create mode 100644 libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.spec.tsx create mode 100644 libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.tsx 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..e69de29bb2d 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..0ba750954f6 --- /dev/null +++ b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx @@ -0,0 +1,254 @@ +import { + type Cluster, + ClusterFeatureKarpenterParameters, + type KarpenterDefaultNodePoolOverride, + type KarpenterStableNodePoolOverride, + WeekdayEnum, +} from 'qovery-typescript-axios' +import { Controller, FormProvider, useForm } 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' +import { useEditCluster } from '../../hooks/use-edit-cluster/use-edit-cluster' + +export interface NodepoolModalProps { + type: 'stable' | 'default' + cluster: Cluster +} + +const CPU_MIN = 6 +const MEMORY_MIN = 10 + +export function NodepoolModal({ type, cluster }: NodepoolModalProps) { + const { mutateAsync: editCluster, isLoading: isLoadingEditCluster } = useEditCluster() + const { closeModal } = useModal() + + const karpenterNodePools = ( + cluster.features?.find((feature) => feature.id === 'KARPENTER')?.value_object + ?.value as ClusterFeatureKarpenterParameters + ).qovery_node_pools + + const methods = useForm({ + mode: 'onChange', + defaultValues: type === 'stable' ? karpenterNodePools.stable_override : karpenterNodePools.default_override, + }) + + console.log(karpenterNodePools) + + const watchConsolidation = methods.watch('consolidation.enabled') + + const onSubmit = methods.handleSubmit(async (data) => { + // TODO: Fix duration format and other format issues + try { + await editCluster({ + organizationId: cluster.organization.id, + clusterId: cluster.id, + clusterRequest: { + ...cluster, + features: cluster.features?.map((feature) => { + if (feature.id === 'KARPENTER') { + return { + id: 'KARPENTER', + value: { + ...(feature.value_object?.value as ClusterFeatureKarpenterParameters), + ...match({ type, data }) + .with( + { + type: 'stable', + data: P.when((d): d is KarpenterStableNodePoolOverride => 'consolidation' in d), + }, + ({ data }) => { + return { + qovery_node_pools: { + ...karpenterNodePools, + stable_override: { + ...data, + consolidation: { + ...data.consolidation, + enabled: data.consolidation?.enabled ?? false, + duration: `PT${data.consolidation?.duration}`, + }, + }, + }, + } + } + ) + .with({ type: 'default' }, ({ data }) => ({ + ...karpenterNodePools, + default_override: data, + })) + .exhaustive(), + }, + } + } + return feature + }), + }, + }) + + // closeModal() + } catch (error) { + console.error(error) + } + }) + + const daysOptions = Object.keys(WeekdayEnum).map((key) => ({ + label: upperCaseFirstLetter(key), + value: key, + })) + + return ( + + +
    +
    +

    Nodepool resources limits

    + + + + + +
    + ( + + )} + /> + ( + + )} + /> +
    +
    + {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. + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> +
    + )} + + )} +
    +
    +
    + ) +} + +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..e69de29bb2d 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..ccf712cec1e --- /dev/null +++ b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepools-resources-settings.tsx @@ -0,0 +1,76 @@ +import { type Cluster } from 'qovery-typescript-axios' +import { BlockContent, Button, Icon, Tooltip, useModal } from '@qovery/shared/ui' +import NodepoolModal from './nodepool-modal/nodepool-modal' + +export interface NodepoolsResourcesSettingsProps { + cluster: Cluster +} + +export function NodepoolsResourcesSettings({ cluster }: NodepoolsResourcesSettingsProps) { + const { openModal } = useModal() + + return ( + + + + + + } + > +
    +
    +

    Stable nodepool

    + + Used for single instances and internal Qovery applications, such as containerized databases, to maintain + stability. + +
    +
    + +
    +
    +
    +
    +

    Default nodepool

    + + Designed to handle general workloads and serves as the foundation for deploying most applications. + +
    +
    + +
    +
    +
    + ) +} + +export default NodepoolsResourcesSettings 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 b46f5dbd23c..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 { @@ -340,6 +341,8 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { )} + {watchKarpenterEnabled && props.cluster && } + {!watchKarpenterEnabled && (
    Resources configuration 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}
    } From 96d673a3b048665d2c8c61be602ff9e41d2c4627 Mon Sep 17 00:00:00 2001 From: Remi Bonnet Date: Tue, 14 Jan 2025 18:17:06 +0100 Subject: [PATCH 05/17] Add edit cluster for nodepool behavior --- .../nodepool-modal/nodepool-modal.tsx | 202 +++++++++--------- .../nodepools-resources-settings.tsx | 136 +++++++++++- .../ui/src/lib/styles/components/input.scss | 4 + 3 files changed, 231 insertions(+), 111 deletions(-) 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 index 0ba750954f6..9ee846548a1 100644 --- 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 @@ -1,26 +1,73 @@ import { type Cluster, - ClusterFeatureKarpenterParameters, - type KarpenterDefaultNodePoolOverride, - type KarpenterStableNodePoolOverride, + type ClusterFeatureKarpenterParameters, + KarpenterDefaultNodePoolOverride, + type KarpenterNodePool, + KarpenterStableNodePoolOverride, WeekdayEnum, } from 'qovery-typescript-axios' -import { Controller, FormProvider, useForm } from 'react-hook-form' -import { P, match } from 'ts-pattern' +import { Controller, FormProvider, useForm, useFormContext } from 'react-hook-form' import { Callout, Icon, InputSelect, InputText, InputToggle, ModalCrud, Tooltip, useModal } from '@qovery/shared/ui' import { upperCaseFirstLetter } from '@qovery/shared/util-js' -import { useEditCluster } from '../../hooks/use-edit-cluster/use-edit-cluster' + +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: KarpenterNodePool) => void + defaultValues?: KarpenterStableNodePoolOverride | KarpenterDefaultNodePoolOverride } const CPU_MIN = 6 const MEMORY_MIN = 10 -export function NodepoolModal({ type, cluster }: NodepoolModalProps) { - const { mutateAsync: editCluster, isLoading: isLoadingEditCluster } = useEditCluster() +export function NodepoolModal({ type, cluster, onChange, defaultValues }: NodepoolModalProps) { const { closeModal } = useModal() const karpenterNodePools = ( @@ -28,68 +75,49 @@ export function NodepoolModal({ type, cluster }: NodepoolModalProps) { ?.value as ClusterFeatureKarpenterParameters ).qovery_node_pools - const methods = useForm({ + const methods = useForm>({ mode: 'onChange', - defaultValues: type === 'stable' ? karpenterNodePools.stable_override : karpenterNodePools.default_override, + defaultValues: { + default_override: defaultValues, + stable_override: defaultValues, + }, }) - console.log(karpenterNodePools) - - const watchConsolidation = methods.watch('consolidation.enabled') + const watchConsolidation = methods.watch('stable_override.consolidation.enabled') const onSubmit = methods.handleSubmit(async (data) => { - // TODO: Fix duration format and other format issues - try { - await editCluster({ - organizationId: cluster.organization.id, - clusterId: cluster.id, - clusterRequest: { - ...cluster, - features: cluster.features?.map((feature) => { - if (feature.id === 'KARPENTER') { - return { - id: 'KARPENTER', - value: { - ...(feature.value_object?.value as ClusterFeatureKarpenterParameters), - ...match({ type, data }) - .with( - { - type: 'stable', - data: P.when((d): d is KarpenterStableNodePoolOverride => 'consolidation' in d), - }, - ({ data }) => { - return { - qovery_node_pools: { - ...karpenterNodePools, - stable_override: { - ...data, - consolidation: { - ...data.consolidation, - enabled: data.consolidation?.enabled ?? false, - duration: `PT${data.consolidation?.duration}`, - }, - }, - }, - } - } - ) - .with({ type: 'default' }, ({ data }) => ({ - ...karpenterNodePools, - default_override: data, - })) - .exhaustive(), - }, - } - } - return feature + onChange({ + ...karpenterNodePools, + ...(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() - } catch (error) { - console.error(error) - } + closeModal() }) const daysOptions = Object.keys(WeekdayEnum).map((key) => ({ @@ -104,8 +132,7 @@ export function NodepoolModal({ type, cluster }: NodepoolModalProps) { description="Used for single instances and internal Qovery applications, such as containerized databases, to maintain stability." onSubmit={onSubmit} onClose={closeModal} - submitLabel="Save" - loading={isLoadingEditCluster} + submitLabel="Confirm" >
    @@ -119,42 +146,7 @@ export function NodepoolModal({ type, cluster }: NodepoolModalProps) {
    - ( - - )} - /> - ( - - )} - /> +
    {type === 'default' && ( @@ -175,7 +167,7 @@ export function NodepoolModal({ type, cluster }: NodepoolModalProps) { {type === 'stable' && ( <> ( Some downtime may occur during this process. ( @@ -212,7 +204,7 @@ export function NodepoolModal({ type, cluster }: NodepoolModalProps) { )} /> ( @@ -227,7 +219,7 @@ export function NodepoolModal({ type, cluster }: NodepoolModalProps) { )} /> ( 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 index ccf712cec1e..86e3f51be3c 100644 --- 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 @@ -1,6 +1,62 @@ -import { type Cluster } from 'qovery-typescript-axios' +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 NodepoolModal from './nodepool-modal/nodepool-modal' +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) 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) { + return `${upperCaseFirstLetter(days[0])} to ${upperCaseFirstLetter(days[days.length - 1])}` + } + + return days.map(shortenDay).join(', ') +} export interface NodepoolsResourcesSettingsProps { cluster: Cluster @@ -8,6 +64,12 @@ export interface NodepoolsResourcesSettingsProps { 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 (
    -
    +
    +
    + {watchStable?.consolidation?.days && ( + + + {formatWeekdays(watchStable?.consolidation?.days)}, + + + + + + + + {start} to {end} + + + )} + {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 + )} + + )} +
    -
    +
    +
    + + + 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 + )} + +
    -
    +
    - Operates every day, + Consolidation operates every day, @@ -172,14 +174,18 @@ export function NodepoolsResourcesSettings({ cluster }: NodepoolsResourcesSettin 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 limit: {watchDefault?.limits?.max_memory_in_gibibytes} GiB - )} - + {watchDefault?.limits?.enabled ? ( + + {watchDefault?.limits?.max_cpu_in_vcpu && ( + vCPU limit: {watchDefault?.limits?.max_cpu_in_vcpu} vCPU; + )} + {watchDefault?.limits?.max_memory_in_gibibytes && ( + Memory limit: {watchDefault?.limits?.max_memory_in_gibibytes} GiB + )} + + ) : ( + No resource limit + )}