Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions apps/console-v5/src/routeTree.gen.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { AnnotationSetting, LabelSetting } from '@qovery/domains/organizations/feature'
import { HelmStepGeneral } from '@qovery/domains/service-helm/feature'
import { serviceCreateParamsSchema } from '@qovery/shared/router'
import { useDocumentTitle } from '@qovery/shared/util-hooks'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/general'
)({
component: General,
validateSearch: serviceCreateParamsSchema,
})

function General() {
const { organizationId = '', projectId = '', environmentId = '' } = Route.useParams()
const search = Route.useSearch()
const navigate = useNavigate()
const creationFlowUrl = `/organization/${organizationId}/project/${projectId}/environment/${environmentId}/service/create/helm`

useDocumentTitle('General - Create Helm')

return (
<HelmStepGeneral
onSubmit={() => navigate({ to: `${creationFlowUrl}/values-override-file`, search })}
labelSetting={<LabelSetting />}
annotationSetting={<AnnotationSetting />}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Navigate, createFileRoute, useParams } from '@tanstack/react-router'
import { serviceCreateParamsSchema } from '@qovery/shared/router'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/'
)({
component: RouteComponent,
validateSearch: serviceCreateParamsSchema,
})

function RouteComponent() {
const { organizationId = '', projectId = '', environmentId = '' } = useParams({ strict: false })
const search = Route.useSearch()

return (
<Navigate
to="/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/general"
params={{ organizationId, projectId, environmentId }}
search={search}
replace
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Outlet, createFileRoute, useParams } from '@tanstack/react-router'
import { HelmCreationFlow } from '@qovery/domains/service-helm/feature'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm'
)({
component: RouteComponent,
})

function RouteComponent() {
const { organizationId = '', projectId = '', environmentId = '' } = useParams({ strict: false })
const creationFlowUrl = `/organization/${organizationId}/project/${projectId}/environment/${environmentId}/service/create/helm`

return (
<HelmCreationFlow creationFlowUrl={creationFlowUrl}>
<Outlet />
</HelmCreationFlow>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Navigate, createFileRoute } from '@tanstack/react-router'
import { HelmStepValuesOverrideFile, useHelmCreateContext } from '@qovery/domains/service-helm/feature'
import { serviceCreateParamsSchema } from '@qovery/shared/router'
import { useDocumentTitle } from '@qovery/shared/util-hooks'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/values-override-file'
)({
component: ValuesOverrideFile,
validateSearch: serviceCreateParamsSchema,
})

function ValuesOverrideFile() {
const { organizationId = '', projectId = '', environmentId = '' } = Route.useParams()
const search = Route.useSearch()
const { generalForm } = useHelmCreateContext()
const generalValues = generalForm.getValues()

useDocumentTitle('Values override as file - Create Helm')

if (!generalValues.name || !generalValues.source_provider) {
return (
<Navigate
to="/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm/general"
params={{
organizationId,
projectId,
environmentId,
}}
search={search}
replace
/>
)
}

return <HelmStepValuesOverrideFile />
}
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ const bypassLayoutRouteIds: FileRouteTypes['id'][] = [
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/monitoring/alerts/$alertId/edit',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/$slug',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/database',
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/create/helm',
]

function useBypassLayout(): boolean {
Expand Down
3 changes: 3 additions & 0 deletions libs/domains/service-helm/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ export * from './lib/hooks/use-helm-repositories/use-helm-repositories'
export * from './lib/hooks/use-create-helm-service/use-create-helm-service'
export * from './lib/hooks/use-helm-default-values/use-helm-default-values'
export * from './lib/hooks/use-kubernetes-services/use-kubernetes-services'
export * from './lib/helm-creation-flow/helm-creation-flow'
export * from './lib/helm-creation-flow/step-general/step-general'
export * from './lib/helm-creation-flow/step-values-override-files/step-values-override-files'
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { serviceTemplates } from '@qovery/domains/services/feature'

export interface HelmTemplateMatch {
templateTitle?: string
iconUri?: string
}

export function findHelmTemplateMatch(template?: string): HelmTemplateMatch {
if (!template) {
return {}
}

const helmTemplate = serviceTemplates.find((serviceTemplate) => serviceTemplate.slug === template)

return {
templateTitle: helmTemplate?.title,
iconUri: helmTemplate?.icon_uri,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { HelmCreationFlow, useHelmCreateContext } from './helm-creation-flow'

const mockNavigate = jest.fn()
const mockSearch = {
template: 'kubecost',
}

jest.mock('@qovery/shared/assistant/feature', () => ({
AssistantTrigger: () => null,
}))

jest.mock('@tanstack/react-router', () => ({
...jest.requireActual('@tanstack/react-router'),
useParams: () => ({
organizationId: 'org-1',
projectId: 'proj-1',
environmentId: 'env-1',
}),
useNavigate: () => mockNavigate,
useSearch: () => mockSearch,
}))

function ContextConsumer() {
const { currentStep, creationFlowUrl, generalForm, valuesOverrideFileForm } = useHelmCreateContext()

return (
<div data-testid="context-consumer">
step={currentStep} url={creationFlowUrl} name={generalForm.getValues('name')} icon=
{generalForm.getValues('icon_uri')} valuesType={valuesOverrideFileForm.getValues('type')}
</div>
)
}

describe('HelmCreationFlow', () => {
it('renders and provides helm creation context', () => {
renderWithProviders(
<HelmCreationFlow creationFlowUrl="/create/helm">
<ContextConsumer />
</HelmCreationFlow>
)

expect(screen.getByText('General data')).toBeInTheDocument()
expect(screen.getByTestId('context-consumer')).toHaveTextContent('step=1')
expect(screen.getByTestId('context-consumer')).toHaveTextContent('url=/create/helm')
expect(screen.getByTestId('context-consumer')).toHaveTextContent('name=kubecost')
expect(screen.getByTestId('context-consumer')).toHaveTextContent('icon=app://qovery-console/kubecost')
expect(screen.getByTestId('context-consumer')).toHaveTextContent('valuesType=GIT_REPOSITORY')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useNavigate, useParams, useSearch } from '@tanstack/react-router'
import { type Dispatch, type PropsWithChildren, type SetStateAction, createContext, useContext, useState } from 'react'
import { type UseFormReturn, useForm } from 'react-hook-form'
import { type HelmGeneralData } from '@qovery/domains/services/feature'
import { AssistantTrigger } from '@qovery/shared/assistant/feature'
import { FunnelFlow } from '@qovery/shared/ui'
import { type HelmValuesFileData } from '../values-override-files-setting/values-override-files-setting'
import { findHelmTemplateMatch } from './helm-create-utils/helm-create-utils'

export interface HelmCreateContextInterface {
currentStep: number
setCurrentStep: Dispatch<SetStateAction<number>>
creationFlowUrl: string
generalForm: UseFormReturn<HelmGeneralData>
valuesOverrideFileForm: UseFormReturn<HelmValuesFileData>
}

export const HelmCreateContext = createContext<HelmCreateContextInterface | undefined>(undefined)

export const useHelmCreateContext = () => {
const helmCreateContext = useContext(HelmCreateContext)

if (!helmCreateContext) {
throw new Error('useHelmCreateContext must be used within a HelmCreateContext')
}

return helmCreateContext
}

export const helmCreationSteps: { title: string }[] = [{ title: 'General data' }, { title: 'Values override as file' }]

export interface HelmCreationFlowProps extends PropsWithChildren {
creationFlowUrl: string
}

export function HelmCreationFlow({ children, creationFlowUrl }: HelmCreationFlowProps) {
const { organizationId = '', projectId = '', environmentId = '' } = useParams({ strict: false })
const { template } = useSearch({ strict: false })
const navigate = useNavigate()
const [currentStep, setCurrentStep] = useState(1)

const templateMatch = findHelmTemplateMatch(template)

const generalForm = useForm<HelmGeneralData>({
defaultValues: {
name: template ?? '',
icon_uri: templateMatch.iconUri ?? 'app://qovery-console/helm',
arguments: '--wait --atomic --debug',
timeout_sec: 600,
labels_groups: [],
annotations_groups: [],
},
mode: 'onChange',
})

const valuesOverrideFileForm = useForm<HelmValuesFileData>({
defaultValues: {
type: 'GIT_REPOSITORY',
},
mode: 'onChange',
})

return (
<HelmCreateContext.Provider
value={{
currentStep,
setCurrentStep,
creationFlowUrl,
generalForm,
valuesOverrideFileForm,
}}
>
<FunnelFlow
onExit={() => {
if (window.confirm('Do you really want to leave?')) {
navigate({
to: '/organization/$organizationId/project/$projectId/environment/$environmentId/service/new',
params: {
organizationId,
projectId,
environmentId,
},
})
}
}}
totalSteps={helmCreationSteps.length}
currentStep={currentStep}
currentTitle={helmCreationSteps[currentStep - 1]?.title}
>
{children}
<AssistantTrigger defaultOpen />
</FunnelFlow>
</HelmCreateContext.Provider>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { ReactNode } from 'react'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import { HelmCreationFlow } from '../helm-creation-flow'
import { HelmStepGeneral } from './step-general'

const mockOnSubmit = jest.fn()

jest.mock('@qovery/shared/assistant/feature', () => ({
AssistantTrigger: () => null,
}))

jest.mock('@qovery/domains/organizations/feature', () => ({
GitBranchSettings: () => <div data-testid="git-branch-settings" />,
GitProviderSetting: () => <div data-testid="git-provider-setting" />,
GitPublicRepositorySettings: () => <div data-testid="git-public-repository-settings" />,
GitRepositorySetting: () => <div data-testid="git-repository-setting" />,
}))

jest.mock('../../source-setting/source-setting', () => ({
SourceSetting: () => <div data-testid="source-setting" />,
}))

jest.mock('../../deployment-setting/deployment-setting', () => ({
DeploymentSetting: () => <div data-testid="deployment-setting" />,
}))

jest.mock('@tanstack/react-router', () => ({
...jest.requireActual('@tanstack/react-router'),
useParams: () => ({
organizationId: 'org-1',
projectId: 'proj-1',
environmentId: 'env-1',
}),
useSearch: () => ({}),
useNavigate: () => jest.fn(),
}))

jest.mock('@qovery/shared/ui', () => ({
...jest.requireActual('@qovery/shared/ui'),
Link: ({ children, ...props }: { children?: ReactNode; [key: string]: unknown }) => <a {...props}>{children}</a>,
}))

const defaultProps = {
labelSetting: <div data-testid="label-setting">Labels</div>,
annotationSetting: <div data-testid="annotation-setting">Annotations</div>,
onSubmit: mockOnSubmit,
}

function renderStepGeneral() {
return renderWithProviders(
<HelmCreationFlow creationFlowUrl="/create/helm">
<HelmStepGeneral {...defaultProps} />
</HelmCreationFlow>
)
}

describe('HelmStepGeneral', () => {
beforeEach(() => {
mockOnSubmit.mockClear()
})

it('renders the general helm form', () => {
renderStepGeneral()

expect(screen.getByRole('heading', { name: 'General information' })).toBeInTheDocument()
expect(screen.getByRole('heading', { name: 'General' })).toBeInTheDocument()
expect(screen.getByRole('heading', { name: 'Source' })).toBeInTheDocument()
expect(screen.getByTestId('source-setting')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Continue' })).toBeInTheDocument()
})

it('does not render extra labels section before a source is selected', () => {
renderStepGeneral()

expect(screen.queryByRole('heading', { name: 'Extra labels/annotations' })).not.toBeInTheDocument()
})
})
Loading
Loading