Skip to content
Original file line number Diff line number Diff line change
@@ -1,11 +1,112 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute, useParams } from '@tanstack/react-router'
import { Suspense } from 'react'
import { FormProvider, useForm, useFormContext } from 'react-hook-form'
import { match } from 'ts-pattern'
import { TerraformVariablesTable } from '@qovery/domains/service-settings/feature'
import { TerraformVariablesProvider, useTerraformVariablesContext } from '@qovery/domains/service-terraform/feature'
import { type Terraform } from '@qovery/domains/services/data-access'
import { type TerraformGeneralData, useEditService, useService } from '@qovery/domains/services/feature'
import { SettingsHeading } from '@qovery/shared/console-shared'
import { Button, LoaderSpinner, Section } from '@qovery/shared/ui'
import { buildEditServicePayload } from '@qovery/shared/util-services'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings/terraform-variables'
)({
component: RouteComponent,
})

const TerraformVariablesLoader = () => (
<div className="flex min-h-page-container items-center justify-center">
<LoaderSpinner />
</div>
)

const TerraformVariablesSettingsForm = ({ service }: { service: Terraform }) => {
const { organizationId = '', projectId = '', environmentId = '' } = Route.useParams()
const { handleSubmit } = useFormContext<TerraformGeneralData>()
const { serializeForApi, tfVarFiles, errors } = useTerraformVariablesContext()

const { mutate: editService, isLoading: isLoadingEditService } = useEditService({
organizationId,
projectId,
environmentId,
})

if (service?.serviceType !== 'TERRAFORM') {
return null
}

const onSubmit = handleSubmit(() => {
// Edit the service with the updated variables and the updated order of tfvars files
const payload = buildEditServicePayload({
service,
request: {
terraform_variables_source: {
...service.terraform_variables_source,
tf_vars: serializeForApi(),
tf_var_file_paths: [...tfVarFiles.filter((file) => file.enabled)].reverse().map((file) => file.source),
},
},
})
editService({
serviceId: service.id,
payload,
})
})

return (
<>
<TerraformVariablesTable />
<div className="mt-10 flex justify-end">
<Button type="submit" size="lg" onClick={onSubmit} loading={isLoadingEditService} disabled={errors.size > 0}>
Save
</Button>
</div>
</>
)
}

const TerraformVariablesContent = ({ service }: { service: Terraform }) => {
const methods = useForm<TerraformGeneralData>({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to pass the ‎TERRAFORM service as a prop. That would avoid having to use ‎match(service) or ‎if (service?.serviceType === 'TERRAFORM') inside this component because it's only related to terraform a not other services

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done here 5c729ea

mode: 'onChange',
defaultValues: match(service)
.with({ serviceType: 'TERRAFORM' }, (s) => s)
.otherwise(() => ({})),
})

return (
<Section className="px-8 pb-8 pt-6">
<SettingsHeading
title="Terraform variables"
description="Select .tfvars files and configure variable values for your Terraform deployment"
/>
<div className="w-full">
<FormProvider {...methods}>
<TerraformVariablesProvider>
<TerraformVariablesSettingsForm service={service} />
</TerraformVariablesProvider>
</FormProvider>
</div>
</Section>
)
}

const TerraformVariablesWrapper = () => {
const { serviceId } = useParams({ strict: false })
const { data: service } = useService({ serviceId })

if (service?.serviceType !== 'TERRAFORM') {
return null
}

return <TerraformVariablesContent service={service} />
}

function RouteComponent() {
return <div className="px-10 py-7">Terraform variables</div>
return (
<Suspense fallback={<TerraformVariablesLoader />}>
<TerraformVariablesWrapper />
</Suspense>
)
}
1 change: 1 addition & 0 deletions libs/domains/service-settings/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './lib/service-domain-settings/service-domain-settings/service-dom
export * from './lib/service-deployment-restrictions-settings/service-deployment-restrictions-settings/service-deployment-restrictions-settings'
export * from './lib/terraform-configuration-settings/terraform-configuration-settings'
export * from './lib/terraform-arguments-settings/terraform-arguments-settings'
export * from './lib/terraform-variables-settings/terraform-variables-table/terraform-variables-table'
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { FormProvider, useForm } from 'react-hook-form'
import {
TerraformVariablesContext,
type TerraformVariablesContextType,
} from '@qovery/domains/service-terraform/feature'
import { type TerraformGeneralData } from '@qovery/domains/services/feature'
import { renderWithProviders, screen, within } from '@qovery/shared/util-tests'
import { type TerraformGeneralData } from '../terraform-configuration-settings/terraform-configuration-settings'
import { TerraformVariablesContext, type TerraformVariablesContextType } from '../terraform-variables-context'
import { TfvarsFilesPopover } from './terraform-tfvars-popover'

const WrapperComponent = ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type CheckedState } from '@radix-ui/react-checkbox'
import clsx from 'clsx'
import { Reorder } from 'framer-motion'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { type TfVarsFile, useTerraformVariablesContext } from '@qovery/domains/service-terraform/feature'
import {
Button,
Checkbox,
Expand All @@ -13,8 +15,6 @@ import {
Skeleton,
Tooltip,
} from '@qovery/shared/ui'
import { twMerge } from '@qovery/shared/util-js'
import { type TfVarsFile, useTerraformVariablesContext } from '../terraform-variables-context'

const TfvarItem = ({
file,
Expand All @@ -41,13 +41,21 @@ const TfvarItem = ({
[tfVarFiles, file.source, setTfVarFiles]
)

const isLastItem = useMemo(() => index === tfVarFiles.length - 1, [index, tfVarFiles.length])

useEffect(() => {
setCurrentIndex(index.toString())
}, [index])

return (
<div
className="grid w-full grid-cols-[1fr_70px] items-center justify-between border-b border-neutral-250 px-4 py-3 last:rounded-b-lg last:border-b-0 hover:bg-neutral-100"
className={clsx(
'relative grid w-full grid-cols-[1fr_70px] items-center justify-between rounded-b-md bg-surface-neutral px-4 py-3',
{
'rounded-none after:absolute after:bottom-0 after:h-[1px] after:w-full after:bg-surface-neutral-componentActive':
!isLastItem,
}
)}
onMouseEnter={() => setHoveredRow(file.source)}
onMouseLeave={() => setHoveredRow(undefined)}
>
Expand All @@ -60,14 +68,14 @@ const TfvarItem = ({
className="ml-1 cursor-pointer"
/>
<label className="flex cursor-pointer flex-col gap-0.5 text-sm" htmlFor={file.source}>
<span className="text-neutral-400">{file.source}</span>
<span className="text-xs text-neutral-350">{Object.keys(file.variables).length} variables</span>
<span className="text-neutral">{file.source}</span>
<span className="text-xs text-neutral-subtle">{Object.keys(file.variables).length} variables</span>
</label>
</div>
<div className="flex items-center justify-between">
<Icon iconName="grip-lines" iconStyle="regular" className="text-neutral-350" />
<Icon iconName="grip-lines" iconStyle="regular" className="text-neutral-subtle" />
<div className="flex items-center gap-1.5">
<span className="text-md leading-3 text-neutral-300">#</span>
<span className="text-md leading-3 text-neutral-subtle">#</span>
<InputTextSmall
name="order"
value={currentIndex}
Expand Down Expand Up @@ -142,7 +150,7 @@ export const TfvarsFilesPopover = () => {
side="left"
content={
<span
className="relative right-0 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-brand-400 text-sm font-bold leading-[0] text-white"
className="absolute -left-1.5 -top-1 flex h-3 w-3 items-center justify-center rounded-full bg-surface-brand-solid text-3xs font-bold leading-[0] text-neutralInvert"
data-testid="enabled-files-count"
>
{enabledFilesCount}
Expand All @@ -161,29 +169,23 @@ export const TfvarsFilesPopover = () => {
</Button>
</Indicator>
) : (
<Button
size="md"
variant="solid"
className="gap-1.5 px-[13px]"
type="button"
data-testid="open-tfvars-files-button"
>
<Button size="md" variant="solid" className="gap-1.5" type="button" data-testid="open-tfvars-files-button">
<Icon iconName="file-lines" iconStyle="regular" />
.tfvars files
</Button>
)}
</div>
</Popover.Trigger>
<Popover.Content side="right" className="flex w-[340px] flex-col rounded-lg border border-neutral-250 p-0">
<Popover.Content side="right" className="flex w-[340px] flex-col rounded-lg border border-neutral p-0">
<div className="flex items-center justify-between px-3 py-2">
<span className="px-1 py-1 text-sm font-medium text-neutral-400">Add and order .tfvars files</span>
<span className="px-1 py-1 text-sm font-medium text-neutral">Add and order .tfvars files</span>
<Popover.Close>
<button type="button" className="flex items-center justify-center px-1 py-1">
<Icon iconName="xmark" className="text-lg font-normal leading-4 text-neutral-350" />
</button>
<Button type="button" iconOnly variant="plain">
<Icon iconName="xmark" className="text-sm font-normal leading-4 text-neutral-subtle" />
</Button>
</Popover.Close>
</div>
<div className="flex flex-col gap-2 border-t border-neutral-250 px-4 py-3">
<div className="flex flex-col gap-2 border-t border-neutral px-4 py-3">
<div className="relative">
<InputTextSmall
name="path"
Expand All @@ -205,35 +207,35 @@ export const TfvarsFilesPopover = () => {
</div>
) : (
<button
className="absolute right-0 top-0 flex h-full w-9 items-center justify-center"
className="absolute right-0 top-0 flex h-full w-9 items-center justify-center text-neutral-subtle hover:text-neutral"
type="button"
onClick={submitNewPath}
>
<Icon iconName="plus" className="text-lg font-normal leading-4 text-neutral-350" />
<Icon iconName="plus" className="text-sm" />
</button>
)}
</div>
{newPathErrorMessage && <div className="text-xs text-red-500">{newPathErrorMessage}</div>}
{newPathErrorMessage && <div className="text-xs text-negative">{newPathErrorMessage}</div>}
</div>
{!areTfVarsFilesLoading && tfVarFiles.length !== 0 && (
<div className="flex items-center justify-between border-t border-neutral-250 bg-neutral-100 px-4 py-1">
<span className="text-xs text-neutral-350">File order defines override priority.</span>
<div className="flex items-center justify-between border-t border-neutral bg-surface-neutral px-4 py-1">
<span className="text-xs text-neutral-subtle">File order defines override priority.</span>
<Tooltip
classNameContent="max-w-[230px]"
content="Files higher in the list override variables from lower ones."
side="left"
>
<span className="text-sm text-neutral-350">
<span className="text-sm text-neutral-subtle">
<Icon iconName="info-circle" iconStyle="regular" />
</span>
</Tooltip>
</div>
)}
<div className="flex flex-col border-t border-neutral-250">
<div className="flex flex-col border-t border-neutral">
{areTfVarsFilesLoading && tfVarFiles.length === 0 ? (
<>
{Array.from({ length: 2 }).map((_, index) => (
<div key={index} className="flex w-full items-center gap-4 border-b border-neutral-250 px-4 py-4">
<div key={index} className="flex w-full items-center gap-4 border-b border-neutral px-4 py-4">
<div className="flex items-center">
<Skeleton height={16} width={16} />
</div>
Expand All @@ -245,16 +247,16 @@ export const TfvarsFilesPopover = () => {
))}
</>
) : tfVarFiles.length > 0 ? (
<ScrollShadowWrapper className="max-h-[300px]">
<ScrollShadowWrapper hideSides className="max-h-[300px]">
<Reorder.Group axis="y" values={tfVarFiles} onReorder={onReorder}>
{tfVarFiles?.map((file, index) => (
<Reorder.Item
key={file.source}
value={file}
initial={{ cursor: 'grab' }}
initial={{ cursor: 'grab', borderColor: 'var(--brand-6)' }}
exit={{ cursor: 'grab' }}
whileDrag={{ cursor: 'grabbing', borderColor: '#642DFF', borderWidth: '2px' }}
className={twMerge('flex w-full items-center border-b border-neutral-250')}
whileDrag={{ cursor: 'grabbing', borderWidth: '2px' }}
className="flex w-full items-center"
data-testid="tfvar-item"
>
<TfvarItem key={file.source} file={file} index={index} onIndexChange={onIndexChange} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { type TfVarsFileResponse } from 'qovery-typescript-axios'
import { FormProvider, useForm } from 'react-hook-form'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { type TerraformGeneralData } from '../terraform-configuration-settings/terraform-configuration-settings'
import {
CUSTOM_SOURCE,
TerraformVariablesContext,
type TerraformVariablesContextType,
} from '../terraform-variables-context'
} from '@qovery/domains/service-terraform/feature'
import { type TerraformGeneralData } from '@qovery/domains/services/feature'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { TerraformVariablesTable } from './terraform-variables-table'

const mockTfVarsFromRepo: TfVarsFileResponse[] = []
Expand Down
Loading
Loading