Skip to content

Commit

Permalink
feat(karpenter-instance-modal): add tooltip for disabled cat and fami…
Browse files Browse the repository at this point in the history
…lies (#1758)

* feat(karpenter-instance-modal): add tooltip for disabled categories and families

* fix: replace architecture before sizes tooltip
  • Loading branch information
RemiBonnet authored Nov 18, 2024
1 parent 6d01749 commit 142c2d9
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { type ClusterInstanceAttributes } from 'qovery-typescript-axios'
import { FormProvider, useForm } from 'react-hook-form'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { InstanceCategory } from './instance-category'
import { type ClusterInstanceAttributesExtended, InstanceCategory } from './instance-category'

const mockAttributes: ClusterInstanceAttributes[] = [
{ instance_family: 't3' },
{ instance_family: 't3a' },
{ instance_family: 't4g' },
const mockAttributes: ClusterInstanceAttributesExtended[] = [
{ instance_family: 't3', sizes: ['t3.small', 't3.medium'], architecture: 'AMD64' },
{ instance_family: 't3a', sizes: ['t3a.small', 't3a.medium'], architecture: 'AMD64' },
{ instance_family: 't4g', sizes: ['t4g.small', 't4g.medium'], architecture: 'AMD64' },
]

const WrapperComponent = ({ children, defaultValues = {} }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { type CheckedState } from '@radix-ui/react-checkbox'
import * as Collapsible from '@radix-ui/react-collapsible'
import { type ClusterInstanceAttributes } from 'qovery-typescript-axios'
import { type ClusterInstanceAttributes, type CpuArchitectureEnum } from 'qovery-typescript-axios'
import { useState } from 'react'
import { Controller, useFormContext } from 'react-hook-form'
import { P, match } from 'ts-pattern'
import { Checkbox, Icon } from '@qovery/shared/ui'
import { Checkbox, Icon, Tooltip } from '@qovery/shared/ui'
import { type KarpenterInstanceFormProps } from '../karpenter-instance-filter-modal'

const getInstanceTypeCategory = (instancePrefix: string): string => {
const prefix = instancePrefix.toLowerCase()
Expand Down Expand Up @@ -38,15 +39,23 @@ const getInstanceTypeCategory = (instancePrefix: string): string => {
return categoryMap[prefix] || 'Unknown'
}

export interface ClusterInstanceAttributesExtended extends ClusterInstanceAttributes {
architecture: CpuArchitectureEnum
sizes: string[]
}

export interface InstanceCategoryProps {
title: string
attributes: ClusterInstanceAttributes[]
attributes: ClusterInstanceAttributesExtended[]
}

export function InstanceCategory({ title, attributes }: InstanceCategoryProps) {
const [open, setOpen] = useState(false)
const { control, watch, setValue } = useFormContext<{ categories: Record<string, string[]> }>()
const { control, watch, setValue } = useFormContext<KarpenterInstanceFormProps>()

const watchSizes = watch('sizes') || []
const watchAMD64 = watch('AMD64')
const watchARM64 = watch('ARM64')
const watchCategories = watch(`categories.${title}`) || []
const validAttributes = attributes.filter((a) => a.instance_family)

Expand All @@ -61,6 +70,49 @@ export function InstanceCategory({ title, attributes }: InstanceCategoryProps) {
)
.otherwise(() => false)

const attributeCheckboxState = (attribute: ClusterInstanceAttributesExtended) => {
const architectureEnabled = match(attribute.architecture)
.with('AMD64', () => watchAMD64)
.with('ARM64', () => watchARM64)
.otherwise(() => false)

const sizeDisabled = !watchSizes.some((size) => attribute.sizes.includes(size))

const getTooltipMessage = () => {
const conditions = []

if (!architectureEnabled) {
conditions.push(<span>{attribute.architecture} architecture must be enabled for this instance type.</span>)
}

if (sizeDisabled) {
const availableSizes = attribute.sizes.join(', ')
conditions.push(
<span>
Selected size not available for this instance.
<br />
Available sizes: {availableSizes}
</span>
)
}

if (conditions.length === 0) return false

return (
<span className="flex flex-col gap-2">
{conditions.map((condition, index) => (
<span key={index}>{condition}</span>
))}
</span>
)
}

return {
disabled: sizeDisabled || !architectureEnabled,
message: getTooltipMessage(),
}
}

return (
<Collapsible.Root key={title} open={open} onOpenChange={setOpen} asChild>
<div className="flex flex-col">
Expand Down Expand Up @@ -92,32 +144,56 @@ export function InstanceCategory({ title, attributes }: InstanceCategoryProps) {

<Collapsible.Content asChild>
<div className="flex flex-col">
{attributes.map((attribute) => (
<div key={attribute.instance_family} className="flex items-center gap-3 py-1 pl-6">
<Controller
name={`categories.${title}`}
control={control}
render={({ field }) => (
<>
<Checkbox
className="shrink-0"
id={`${title}-${attribute.instance_family}`}
checked={field.value?.includes(attribute.instance_family!)}
onCheckedChange={(checked) => {
const newValue = checked
? [...(field.value || []), attribute.instance_family!]
: (field.value || []).filter((v) => v !== attribute.instance_family)
field.onChange(newValue)
}}
/>
<label htmlFor={`${title}-${attribute.instance_family}`} className="text-neutral-400">
{attribute.instance_family}
</label>
</>
{attributes.map((attribute) => {
const { disabled, message } = attributeCheckboxState(attribute)

// Not used `field.value` because it's not updated with select all / unselect all
const value = watch(`categories.${title}`)?.includes(attribute.instance_family!)

return (
<div key={attribute.instance_family}>
{disabled ? (
<Tooltip content={message} side="right">
<div className="flex w-fit items-center gap-3 py-1 pl-6">
<Checkbox
className="shrink-0"
id={`${title}-${attribute.instance_family}`}
checked={false}
disabled
/>
<label htmlFor={`${title}-${attribute.instance_family}`} className="text-neutral-400">
{attribute.instance_family}
</label>
</div>
</Tooltip>
) : (
<Controller
name={`categories.${title}`}
control={control}
render={({ field }) => (
<div className="flex w-fit items-center gap-3 py-1 pl-6">
<Checkbox
className="shrink-0"
id={`${title}-${attribute.instance_family}`}
checked={value}
disabled={disabled}
onCheckedChange={(checked) => {
const newValue = checked
? [...(field.value || []), attribute.instance_family!]
: (field.value || []).filter((v) => v !== attribute.instance_family)
field.onChange(newValue)
}}
/>
<label htmlFor={`${title}-${attribute.instance_family}`} className="text-neutral-400">
{attribute.instance_family}
</label>
</div>
)}
/>
)}
/>
</div>
))}
</div>
)
})}
</div>
</Collapsible.Content>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { pluralize, twMerge } from '@qovery/shared/util-js'
import { useCloudProviderInstanceTypesKarpenter } from '../hooks/use-cloud-provider-instance-types-karpenter/use-cloud-provider-instance-types-karpenter'
import { filterInstancesByKarpenterRequirements } from '../karpenter-instance-filter-modal/utils/filter-instances-by-karpenter-requirements'
import { generateDefaultValues } from '../karpenter-instance-filter-modal/utils/generate-default-values'
import { InstanceCategory } from './instance-category/instance-category'
import { type ClusterInstanceAttributesExtended, InstanceCategory } from './instance-category/instance-category'
import { sortInstanceSizes } from './utils/sort-instance-sizes'

const DISPLAY_LIMIT = 60
Expand Down Expand Up @@ -425,8 +425,22 @@ function KarpenterInstanceForm({
{allCategories
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
.map((category) => {
const attributes: ClusterInstanceAttributes[] = Object.values(instanceCategories).flatMap(
(architecture) => architecture[category] || []
const allSizes = Array.from(
new Set(
Object.values(instanceCategories)
.flatMap((categories) => categories[category] || [])
.filter((attr) => attr.instance_size)
.map((attr) => attr.instance_size!)
)
).sort()

const attributes: ClusterInstanceAttributesExtended[] = Object.entries(instanceCategories).flatMap(
([architecture, categories]) =>
(categories[category] || []).map((attr) => ({
...attr,
architecture: architecture as CpuArchitectureEnum,
sizes: allSizes,
}))
)

if (attributes.length === 0) return null
Expand Down

0 comments on commit 142c2d9

Please sign in to comment.