From 6a952796c40277df2872eeccc7bf3471e0ed5b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Bonnet?= Date: Tue, 29 Oct 2024 14:14:42 +0100 Subject: [PATCH] feat(page-new-cluster): update grid cards (#1740) --- .../__snapshots__/cluster-card.spec.tsx.snap | 2 +- .../src/lib/cluster-card/cluster-card.tsx | 2 +- .../cluster-installation-guide-modal.tsx | 6 +- .../page-clusters-create-feature.tsx | 3 +- .../page-new-feature/page-new-feature.tsx | 378 +++++++++++------- .../step-general/step-general.spec.tsx | 28 -- .../step-general/step-general.tsx | 200 ++++----- libs/shared/ui/src/lib/styles/main.scss | 2 +- 8 files changed, 335 insertions(+), 286 deletions(-) diff --git a/libs/domains/clusters/feature/src/lib/cluster-card/__snapshots__/cluster-card.spec.tsx.snap b/libs/domains/clusters/feature/src/lib/cluster-card/__snapshots__/cluster-card.spec.tsx.snap index d2687430e84..acdb95e04c4 100644 --- a/libs/domains/clusters/feature/src/lib/cluster-card/__snapshots__/cluster-card.spec.tsx.snap +++ b/libs/domains/clusters/feature/src/lib/cluster-card/__snapshots__/cluster-card.spec.tsx.snap @@ -3,7 +3,7 @@ exports[`ClusterCard should render correctly 1`] = `
diff --git a/libs/domains/clusters/feature/src/lib/cluster-installation-guide-modal/cluster-installation-guide-modal.tsx b/libs/domains/clusters/feature/src/lib/cluster-installation-guide-modal/cluster-installation-guide-modal.tsx index feb06d8c88c..b09e232fdfe 100644 --- a/libs/domains/clusters/feature/src/lib/cluster-installation-guide-modal/cluster-installation-guide-modal.tsx +++ b/libs/domains/clusters/feature/src/lib/cluster-installation-guide-modal/cluster-installation-guide-modal.tsx @@ -32,9 +32,13 @@ export function ClusterInstallationGuideModal({ type, onClose, ...props }: Clust download(installationHelmValues ?? '', `cluster-installation-guide-${props.cluster.id}.yaml`, 'text/plain') } + const isDemo = props.mode === 'CREATE' ? props.isDemo : props.cluster.is_demo + return (
-

Installation guide

+

+ {isDemo ? 'Install Qovery on your local machine' : 'Install Qovery on your cluster'} +

{props.mode === 'EDIT' && type === 'ON_PREMISE' && ( 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 472e612d1d8..e62e33c67f8 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 @@ -13,6 +13,7 @@ import { import { CLUSTERS_CREATION_GENERAL_URL, CLUSTERS_CREATION_URL, + CLUSTERS_NEW_URL, CLUSTERS_TEMPLATE_CREATION_URL, CLUSTERS_URL, } from '@qovery/shared/routes' @@ -157,7 +158,7 @@ export function PageClusterCreateFeature() { { if (window.confirm('Do you really want to leave?')) { - navigate(CLUSTERS_URL(organizationId)) + navigate(CLUSTERS_URL(organizationId) + CLUSTERS_NEW_URL) } }} totalSteps={steps(generalData, resourcesData?.cluster_type).length} diff --git a/libs/pages/clusters/src/lib/feature/page-new-feature/page-new-feature.tsx b/libs/pages/clusters/src/lib/feature/page-new-feature/page-new-feature.tsx index 727ba733c73..e8103f6f962 100644 --- a/libs/pages/clusters/src/lib/feature/page-new-feature/page-new-feature.tsx +++ b/libs/pages/clusters/src/lib/feature/page-new-feature/page-new-feature.tsx @@ -4,24 +4,31 @@ import Azure from 'devicon/icons/azure/azure-original.svg' import DigitalOcean from 'devicon/icons/digitalocean/digitalocean-original.svg' import GCP from 'devicon/icons/googlecloud/googlecloud-original.svg' import Kubernetes from 'devicon/icons/kubernetes/kubernetes-original.svg' +import { AnimatePresence, motion } from 'framer-motion' import posthog from 'posthog-js' -import { type CloudProviderEnum } from 'qovery-typescript-axios' -import { type ReactElement, cloneElement, useState } from 'react' +import { CloudProviderEnum } from 'qovery-typescript-axios' +import { type MutableRefObject, type ReactElement, cloneElement, useState } from 'react' import { NavLink, useParams } from 'react-router-dom' import { match } from 'ts-pattern' import { ClusterInstallationGuideModal } from '@qovery/domains/clusters/feature' import { CLUSTERS_TEMPLATE_CREATION_URL, CLUSTERS_URL } from '@qovery/shared/routes' -import { Button, Heading, Icon, Link, Section, useModal } from '@qovery/shared/ui' -import { useDocumentTitle } from '@qovery/shared/util-hooks' +import { Button, ExternalLink, Heading, Icon, Link, Section, Tooltip, useModal } from '@qovery/shared/ui' +import { useClickAway, useDocumentTitle } from '@qovery/shared/util-hooks' +import { twMerge } from '@qovery/shared/util-js' const Qovery = '/assets/logos/logo-icon.svg' +const ExtendedCloudProviderEnum = { + ...CloudProviderEnum, + AZURE: 'AZURE' as const, +} + type CardOptionProps = { title: string description?: string recommended?: boolean icon?: string | ReactElement - selectedCloudProvider: Exclude + selectedCloudProvider: Exclude } & ( | { selectedInstallationType: 'managed' @@ -35,61 +42,102 @@ type CardOptionProps = { function CardOption({ icon, title, description, selectedCloudProvider, recommended, ...props }: CardOptionProps) { const { organizationId = '' } = useParams() + const renderIcon = () => { + return typeof icon === 'string' ? ( + {title} + ) : ( + cloneElement(icon as ReactElement, { className: 'w-[32px] mt-1 select-none' }) + ) + } + + const renderContent = () => ( + + + {title} + {recommended && ( + <> + {selectedCloudProvider === 'AZURE' ? ( + + Follow the release on our{' '} + + product roadmap + + + } + > + + coming soon + + + ) : ( + + recommended + + )} + + )} + + {description} + + ) + + const handleAnalytics = (selectedInstallationType: string) => { + posthog.capture('select-cluster', { + selectedCloudProvider, + selectedInstallationType, + }) + } + + const baseClassNames = + 'flex text-left items-start gap-4 relative rounded shadow border border-neutral-200 outline outline-2 outline-transparent transition-all hover:border-brand-500 -outline-offset-2 hover:outline-brand-500 bg-white p-5 transition w-[calc(100%/2-20px)] lg:w-[calc(100%/3-20px)]' + return match(props) .with({ selectedInstallationType: 'self-managed' }, ({ selectedInstallationType, openInstallationGuideModal }) => ( )) - .with({ selectedInstallationType: 'managed' }, ({ selectedInstallationType }) => ( - - posthog.capture('select-cluster', { - selectedCloudProvider, - selectedInstallationType, - }) - } - > - {typeof icon === 'string' ? ( - {title} - ) : ( - cloneElement(icon as ReactElement, { className: 'w-[24px] mt-1 select-none' }) - )} - - - {title} - {recommended && ( - - Recommended - - )} - - {description} - - - - - - )) + .with({ selectedInstallationType: 'managed' }, ({ selectedInstallationType }) => + selectedCloudProvider === 'AZURE' ? ( +
+ {renderIcon()} + {renderContent()} +
+ ) : ( + handleAnalytics(selectedInstallationType)} + > + {renderIcon()} + {renderContent()} + + ) + ) .exhaustive() } @@ -97,6 +145,7 @@ type CardClusterProps = { title: string description?: string icon: string | ReactElement + index?: number } & ( | { options: CardOptionProps[] @@ -116,71 +165,95 @@ type CardClusterProps = { } ) -function CardCluster({ title, description, icon, ...props }: CardClusterProps) { +function CardCluster({ title, description, icon, index = 1, ...props }: CardClusterProps) { const [expanded, setExpanded] = useState(false) + const ref = useClickAway(() => { + setExpanded(false) + }) as MutableRefObject + if ('options' in props) { const { options } = props + + const getExpanderPosition = (className: string, index: number) => { + const position = (index + 1) % 3 + + return clsx(className, { + '-ml-[calc(100%+20px)] w-[calc(200%+20px)] lg:w-[calc(300%+40px)]': position === 2, // 3n+2 + 'w-[calc(200%+20px)] lg:-ml-[calc(200%+40px)] lg:w-[calc(300%+40px)]': position === 0, // 3n+3 + 'w-[calc(200%+20px)] lg:w-[calc(300%+40px)]': position === 1, // 3n+1 + '-ml-[calc(100%+20px)] lg:-ml-0': index === 3, + }) + } + return ( -
setExpanded(true)} - className={clsx({ - 'flex cursor-pointer items-center gap-6 rounded border border-neutral-200 p-5 shadow-sm transition hover:bg-neutral-100': - true, - 'col-span-3 bg-neutral-100 p-6': expanded, - })} - > - {expanded ? ( -
-
-
- {typeof icon === 'string' ? ( - {title} - ) : ( - cloneElement(icon as ReactElement, { className: 'w-[52px]' }) - )} -
-
-

{title}

-

{description}

-
- -
-
- {options.map((props) => ( - - ))} -
-
- ) : ( - <> - +
+
setExpanded(!expanded)} + className={twMerge( + clsx( + 'flex h-32 cursor-pointer justify-start gap-4 rounded border border-neutral-200 p-5 shadow-sm outline outline-2 outline-transparent transition hover:border-brand-500 hover:-outline-offset-2 hover:outline-brand-500', + { + 'border-brand-500 -outline-offset-2 outline-brand-500': expanded, + } + ) + )} + > +
+
{typeof icon === 'string' ? ( - {title} + {title} ) : ( - cloneElement(icon as ReactElement, { className: 'w-10' }) + cloneElement(icon as ReactElement, { className: 'w-[48px] h-[48px]' }) )} +
+

{title}

+
+
+ + {options.length} -
-
-

{title}

+ +
+
+ + + {expanded && ( + +
+
+
+ {typeof icon === 'string' ? ( + {title} + ) : ( + cloneElement(icon as ReactElement, { className: 'w-[32px] h-[32px]' }) + )} +
+ {title} +
+
+ {options.map((optionProps) => ( + + ))} +
+
-

- Click to select an option -

-
- - )} + + )} +
) } else { @@ -188,7 +261,7 @@ function CardCluster({ title, description, icon, ...props }: CardClusterProps) { return ( ) } @@ -321,10 +401,27 @@ export function PageNewFeature() { }, { title: 'Microsoft Azure', - selectedCloudProvider: 'AZURE', - selectedInstallationType: 'self-managed', + options: [ + { + title: 'Qovery Managed', + description: + 'Qovery will install and manage the Kubernetes cluster and the underlying infrastructure on your cloud provider account.', + icon: Qovery, + recommended: true, + selectedCloudProvider: 'AZURE', + selectedInstallationType: 'managed', + }, + { + title: 'Self-managed', + description: + 'You will manage the infrastructure, including any update/ upgrade. Advanced Kubernetes knowledge required.', + icon: , + selectedCloudProvider: 'AZURE', + selectedInstallationType: 'self-managed', + openInstallationGuideModal, + }, + ], icon: Azure, - openInstallationGuideModal, }, { title: 'OVH Cloud', @@ -378,33 +475,34 @@ export function PageNewFeature() { ] return ( -
+
Back to clusters -
-
- Install new cluster -

- Configure your Qovery cluster to run on your chosen cloud provider. -

+
+
+ Install cluster
-
-
+
+
Qovery on your local machine -

Quickly test and validate the Qovery solution on your computer.

+

Quickly test and validate the Qovery solution on your computer.

+
+
+
-
-
-
+
+
Or choose your hosting mode -

Manage your infrastructure across different hosting mode.

+

Manage your infrastructure across different hosting mode.

+
+
+ {cloudProviders.slice(1).map((props, index) => ( + + ))}
- {cloudProviders.slice(1).map((props) => ( - - ))}
diff --git a/libs/pages/clusters/src/lib/ui/page-clusters-create/step-general/step-general.spec.tsx b/libs/pages/clusters/src/lib/ui/page-clusters-create/step-general/step-general.spec.tsx index 4afba983a9e..fbc2d2bb490 100644 --- a/libs/pages/clusters/src/lib/ui/page-clusters-create/step-general/step-general.spec.tsx +++ b/libs/pages/clusters/src/lib/ui/page-clusters-create/step-general/step-general.spec.tsx @@ -67,32 +67,4 @@ describe('StepGeneral', () => { expect(button).toBeEnabled() expect(props.onSubmit).toHaveBeenCalled() }) - - it('should render local demo cluster', async () => { - renderWithProviders( - wrapWithReactHookForm(, { - defaultValues: { - installation_type: 'LOCAL_DEMO', - }, - }) - ) - - screen.getByText('1. Download/Update Qovery CLI') - screen.getByText('2. Install your cluster') - screen.getByText('3. Deploy your first environment!') - }) - - it('should render self managed cluster', async () => { - renderWithProviders( - wrapWithReactHookForm(, { - defaultValues: { - installation_type: 'SELF_MANAGED', - }, - }) - ) - - screen.getByText('1. Download/Update Qovery CLI') - screen.getByText('2. Install Qovery on your cluster') - screen.getByText('3. Deploy your first environment!') - }) }) diff --git a/libs/pages/clusters/src/lib/ui/page-clusters-create/step-general/step-general.tsx b/libs/pages/clusters/src/lib/ui/page-clusters-create/step-general/step-general.tsx index 8a69fa752c0..3ef37fae464 100644 --- a/libs/pages/clusters/src/lib/ui/page-clusters-create/step-general/step-general.tsx +++ b/libs/pages/clusters/src/lib/ui/page-clusters-create/step-general/step-general.tsx @@ -2,8 +2,6 @@ import { type CloudProvider, CloudProviderEnum, type ClusterRegion } from 'qover import { type FormEventHandler, useEffect, useMemo, useState } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { useNavigate, useParams } from 'react-router-dom' -import { match } from 'ts-pattern' -import { ClusterSetup } from '@qovery/domains/clusters/feature' import { ClusterCredentialsSettingsFeature, ClusterGeneralSettings } from '@qovery/shared/console-shared' import { type ClusterGeneralData, type ClusterResourcesData, type Value } from '@qovery/shared/interfaces' import { CLUSTERS_URL } from '@qovery/shared/routes' @@ -75,121 +73,97 @@ export function StepGeneral(props: StepGeneralProps) {
- {watch('installation_type') === 'MANAGED' && ( - <> -
-

General

- -
-
-

Provider credentials

- {cloudProviders.length > 0 ? ( - <> - {watch('cloud_provider') === CloudProviderEnum.GCP && ( - - - - - - GCP integration is beta, keep an eye on your cluster costs and report any bugs and/or weird - behavior. - - Setup budget alerts - - - More details - - - - )} - ( - { - field.onChange(value) - setResourcesData && setResourcesData(defaultResourcesData) - }} - value={field.value} - error={error?.message} - portal - /> - )} - /> - {currentProvider && ( - <> - ( - - )} - /> - - - )} - - ) : ( -
- -
- )} -
- - )} - - {watch('installation_type') !== 'MANAGED' && ( + <>
-

Installation instruction

- {watch('installation_type') === 'LOCAL_DEMO' ? ( - - See documentation - +

General

+ +
+
+

Provider credentials

+ {cloudProviders.length > 0 ? ( + <> + {watch('cloud_provider') === CloudProviderEnum.GCP && ( + + + + + + GCP integration is beta, keep an eye on your cluster costs and report any bugs and/or weird + behavior. + + Setup budget alerts + + + More details + + + + )} + ( + { + field.onChange(value) + setResourcesData && setResourcesData(defaultResourcesData) + }} + value={field.value} + error={error?.message} + portal + /> + )} + /> + {currentProvider && ( + <> + ( + + )} + /> + + + )} + ) : ( - - See documentation - +
+ +
)} - - {match(watch('installation_type')) - .with('LOCAL_DEMO', 'SELF_MANAGED', (type) => ) - .otherwise(() => null)}
- )} +