Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { JobConfiguration } from '@qovery/domains/service-settings/feature'
import { useDocumentTitle } from '@qovery/shared/util-hooks'

export const Route = createFileRoute(
'/_authenticated/organization/$organizationId/project/$projectId/environment/$environmentId/service/$serviceId/settings/configure'
Expand All @@ -7,5 +9,7 @@ export const Route = createFileRoute(
})

function RouteComponent() {
return <div className="px-10 py-7">Configuration</div>
useDocumentTitle('Job configuration - Service settings')
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Document title hardcoded, wrong for lifecycle jobs

Medium Severity

The useDocumentTitle call is hardcoded to 'Job configuration - Service settings', but the JobConfiguration component dynamically renders the heading as either "Job configuration" (for CRON jobs) or "Triggers" (for LIFECYCLE jobs). For lifecycle jobs, the browser tab title will incorrectly say "Job configuration" while the page heading says "Triggers".

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8093dae. Configure here.

Comment thread
rmnbrd marked this conversation as resolved.

return <JobConfiguration />
}
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 @@ -13,3 +13,4 @@ export * from './lib/terraform-variables-settings/terraform-variables-table/terr
export * from './lib/helm-values-override-file-settings/helm-values-override-file-settings'
export * from './lib/helm-values-override-arguments-settings/helm-values-override-arguments-settings'
export * from './lib/helm-networking-settings/helm-networking-settings'
export * from './lib/job-configuration/job-configuration'
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form
import { ServiceTypeEnum } from '@qovery/shared/enums'
import { type JobConfigureData, type JobGeneralData } from '@qovery/shared/interfaces'
import { renderWithProviders, screen } from '@qovery/shared/util-tests'
import JobConfigureSettings, { type JobConfigureSettingsProps } from './job-configure-settings'
import { JobConfigurationForm, type JobConfigurationFormProps } from './job-configuration-form'

const props: JobConfigureSettingsProps = {
const props: JobConfigurationFormProps = {
jobType: ServiceTypeEnum.CRON_JOB,
}

Expand All @@ -19,10 +19,10 @@ const defaultValues: JobConfigureData & Pick<JobGeneralData, 'image_entry_point'
max_duration: 0,
}

describe('JobConfigureSettings', () => {
describe('JobConfigurationForm', () => {
it('should render successfully', () => {
const { baseElement } = renderWithProviders(
wrapWithReactHookForm(<JobConfigureSettings {...props} />, {
wrapWithReactHookForm(<JobConfigurationForm {...props} />, {
defaultValues,
})
)
Expand All @@ -34,7 +34,7 @@ describe('JobConfigureSettings', () => {

it('should render 3 enabled box and 3 inputs', () => {
renderWithProviders(
wrapWithReactHookForm(<JobConfigureSettings jobType={ServiceTypeEnum.LIFECYCLE_JOB} />, {
wrapWithReactHookForm(<JobConfigurationForm jobType={ServiceTypeEnum.LIFECYCLE_JOB} />, {
defaultValues,
})
)
Expand All @@ -49,7 +49,7 @@ describe('JobConfigureSettings', () => {

it('should render 4 inputs and 1 select', async () => {
renderWithProviders(
wrapWithReactHookForm(<JobConfigureSettings jobType={ServiceTypeEnum.CRON_JOB} />, {
wrapWithReactHookForm(<JobConfigurationForm jobType={ServiceTypeEnum.CRON_JOB} />, {
defaultValues,
})
)
Expand All @@ -60,7 +60,7 @@ describe('JobConfigureSettings', () => {

it('should display the cron value in a human readable way', async () => {
const { userEvent } = renderWithProviders(
wrapWithReactHookForm(<JobConfigureSettings jobType={ServiceTypeEnum.CRON_JOB} />, {
wrapWithReactHookForm(<JobConfigurationForm jobType={ServiceTypeEnum.CRON_JOB} />, {
defaultValues,
})
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
// TODO: Refactor cronstrue usage to only use formatCronExpression
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import cronstrue from 'cronstrue'
import { Controller, useFormContext } from 'react-hook-form'
import { TimezoneSetting } from '@qovery/domains/services/feature'
import { EntrypointCmdInputs } from '@qovery/shared/console-shared'
import { type JobType, ServiceTypeEnum } from '@qovery/shared/enums'
import { type JobConfigureData } from '@qovery/shared/interfaces'
import { Callout, EnableBox, ExternalLink, Heading, Icon, InputText, LoaderSpinner, Section } from '@qovery/shared/ui'
import { Callout, EnableBox, ExternalLink, Heading, Icon, InputText, Section } from '@qovery/shared/ui'
import { formatCronExpression } from '@qovery/shared/util-js'
import EntrypointCmdInputs from '../../entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs'

export interface JobConfigureSettingsProps {
export interface JobConfigurationFormProps {
jobType: JobType
loading?: boolean
}

export function JobConfigureSettings(props: JobConfigureSettingsProps) {
const { loading } = props
export function JobConfigurationForm(props: JobConfigurationFormProps) {
const { control, watch } = useFormContext<JobConfigureData>()

const watchSchedule = watch('schedule')
const watchTimezone = watch('timezone') ?? 'Etc/UTC'
const watchMaxDuration = watch('max_duration')

return loading ? (
<LoaderSpinner />
) : (
return (
<>
{props.jobType === ServiceTypeEnum.CRON_JOB ? (
<Section className="gap-4">
Expand Down Expand Up @@ -62,71 +57,75 @@ export function JobConfigureSettings(props: JobConfigureSettingsProps) {
<TimezoneSetting />
</Section>
) : (
<Section className="gap-4">
<Heading>Events</Heading>
<p className="text-sm text-neutral-350">
Select one or more event where the job should be executed and the command to execute.
</p>
<Section className="space-y-4">
<div>
<Heading>Events</Heading>
<p className="text-sm text-neutral-subtle">
Select one or more event where the job should be executed and the command to execute.
</p>
</div>

<Controller
name="on_start.enabled"
control={control}
render={({ field }) => (
<EnableBox
className="flex flex-col gap-4"
checked={field.value}
title="Deploy"
name="on_start"
description="Execute this job when the environment/job is deployed or re-deployed"
setChecked={field.onChange}
>
<EntrypointCmdInputs
cmdArgumentsFieldName="on_start.arguments_string"
imageEntryPointFieldName="on_start.entrypoint"
/>
</EnableBox>
)}
/>
<div className="space-y-4">
<Controller
name="on_start.enabled"
control={control}
render={({ field }) => (
<EnableBox
className="flex flex-col gap-4"
checked={field.value}
title="Deploy"
name="on_start"
description="Execute this job when the environment/job is deployed or re-deployed"
setChecked={field.onChange}
>
<EntrypointCmdInputs
cmdArgumentsFieldName="on_start.arguments_string"
imageEntryPointFieldName="on_start.entrypoint"
/>
</EnableBox>
)}
/>

<Controller
name="on_stop.enabled"
control={control}
render={({ field }) => (
<EnableBox
className="flex flex-col gap-4"
checked={field.value}
title="Stop"
name="on_stop"
description="Execute this job when the environment/job stops"
setChecked={field.onChange}
>
<EntrypointCmdInputs
cmdArgumentsFieldName="on_stop.arguments_string"
imageEntryPointFieldName="on_stop.entrypoint"
/>
</EnableBox>
)}
/>
<Controller
name="on_stop.enabled"
control={control}
render={({ field }) => (
<EnableBox
className="flex flex-col gap-4"
checked={field.value}
title="Stop"
name="on_stop"
description="Execute this job when the environment/job stops"
setChecked={field.onChange}
>
<EntrypointCmdInputs
cmdArgumentsFieldName="on_stop.arguments_string"
imageEntryPointFieldName="on_stop.entrypoint"
/>
</EnableBox>
)}
/>

<Controller
name="on_delete.enabled"
control={control}
render={({ field }) => (
<EnableBox
className="flex flex-col gap-4"
checked={field.value}
title="Delete"
name="on_delete"
description="Execute this job when the environment/job is uninstalled or deleted"
setChecked={field.onChange}
>
<EntrypointCmdInputs
cmdArgumentsFieldName="on_delete.arguments_string"
imageEntryPointFieldName="on_delete.entrypoint"
/>
</EnableBox>
)}
/>
<Controller
name="on_delete.enabled"
control={control}
render={({ field }) => (
<EnableBox
className="flex flex-col gap-4"
checked={field.value}
title="Delete"
name="on_delete"
description="Execute this job when the environment/job is uninstalled or deleted"
setChecked={field.onChange}
>
<EntrypointCmdInputs
cmdArgumentsFieldName="on_delete.arguments_string"
imageEntryPointFieldName="on_delete.entrypoint"
/>
</EnableBox>
)}
/>
</div>
</Section>
)}

Expand Down Expand Up @@ -201,5 +200,3 @@ export function JobConfigureSettings(props: JobConfigureSettingsProps) {
</>
)
}

export default JobConfigureSettings
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { type Job } from '@qovery/domains/services/data-access'
import * as servicesDomain from '@qovery/domains/services/feature'
import { cronjobFactoryMock, lifecycleJobFactoryMock } from '@qovery/shared/factories'
import { Section } from '@qovery/shared/ui'
import { renderWithProviders, screen, waitFor } from '@qovery/shared/util-tests'
import PageSettingsConfigureJobFeature from './page-settings-configure-job-feature'
import { JobConfiguration } from './job-configuration'

const useServiceSpy = jest.spyOn(servicesDomain, 'useService') as jest.Mock
const useEditServiceSpy = jest.spyOn(servicesDomain, 'useEditService') as jest.Mock
Expand All @@ -11,6 +12,24 @@ const mockJobApplication = cronjobFactoryMock(1)[0] as Job
const mockLifecycleJobApplication = lifecycleJobFactoryMock(1)[0] as Job
const mockEditService = jest.fn()

jest.mock('@tanstack/react-router', () => ({
...jest.requireActual('@tanstack/react-router'),
useParams: () => ({
organizationId: 'org-1',
projectId: 'project-1',
environmentId: 'env-1',
serviceId: 'service-1',
}),
}))

const render = () => {
return renderWithProviders(
<Section>
<JobConfiguration />
</Section>
)
}

describe('PageSettingsPortsFeature with CRON JOB service', () => {
beforeEach(() => {
useServiceSpy.mockReturnValue({
Expand All @@ -23,7 +42,7 @@ describe('PageSettingsPortsFeature with CRON JOB service', () => {
})

it('should call edit service with correct payload', async () => {
const { userEvent } = renderWithProviders(<PageSettingsConfigureJobFeature />)
const { userEvent } = render()

const inputCron = screen.getByLabelText('Schedule - Cron expression')
const inputNbRestarts = screen.getByLabelText('Number of restarts')
Expand Down Expand Up @@ -72,7 +91,7 @@ describe('PageSettingsPortsFeature with LIFECYCLE JOB service', () => {
})
})
it('should call edit service with correct payload', async () => {
const { userEvent } = renderWithProviders(<PageSettingsConfigureJobFeature />)
const { userEvent } = render()

const checkboxDelete = screen.getByLabelText('Delete')
await userEvent.click(checkboxDelete)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useParams } from '@tanstack/react-router'
import { FormProvider, useForm } from 'react-hook-form'
import { useParams } from 'react-router-dom'
import { match } from 'ts-pattern'
import { useEditService, useService } from '@qovery/domains/services/feature'
import { SettingsHeading } from '@qovery/shared/console-shared'
import { ServiceTypeEnum } from '@qovery/shared/enums'
import { type JobConfigureData, type JobGeneralData } from '@qovery/shared/interfaces'
import { Button, Section } from '@qovery/shared/ui'
import { joinArgsWithQuotes, parseCmd } from '@qovery/shared/util-js'
import PageSettingsConfigureJob from '../../ui/page-settings-configure-job/page-settings-configure-job'
import { JobConfigurationForm } from './job-configuration-form/job-configuration-form'

export function PageSettingsConfigureJobFeature() {
const { organizationId = '', projectId = '', environmentId = '', applicationId = '' } = useParams()

const { data: service } = useService({ serviceId: applicationId, serviceType: 'JOB' })
export const JobConfiguration = () => {
const { organizationId = '', projectId = '', environmentId = '', serviceId = '' } = useParams({ strict: false })
const { data: service } = useService({ serviceId, serviceType: 'JOB', suspense: true })
const { mutate: editService, isLoading: isLoadingEditService } = useEditService({
organizationId,
projectId,
Expand Down Expand Up @@ -121,10 +123,48 @@ export function PageSettingsConfigureJobFeature() {
if (!service) return

return (
<FormProvider {...methods}>
<PageSettingsConfigureJob service={service} loading={isLoadingEditService} onSubmit={onSubmit} />
</FormProvider>
<div className="flex w-full flex-col justify-between">
<Section className="px-8 pb-8 pt-6">
<FormProvider {...methods}>
<div className="flex w-full flex-col justify-between">
<div>
<SettingsHeading
title={match(service)
.with({ service_type: 'JOB', job_type: 'CRON' }, () => 'Job configuration')
.with({ service_type: 'JOB', job_type: 'LIFECYCLE' }, () => 'Triggers')
.otherwise(() => '')}
description={match(service)
.with(
{ service_type: 'JOB', job_type: 'CRON' },
() => 'Job configuration allows you to control the behavior of your service.'
)
.with(
{ service_type: 'JOB', job_type: 'LIFECYCLE' },
() => 'Define the events triggering the execution of this job and the commands to execute.'
)
.otherwise(() => '')}
/>
<div className="max-w-content-with-navigation-left">
<form onSubmit={onSubmit} className="space-y-10">
<JobConfigurationForm
jobType={service.job_type === 'CRON' ? ServiceTypeEnum.CRON_JOB : ServiceTypeEnum.LIFECYCLE_JOB}
/>
<div className="flex justify-end">
<Button
type="submit"
size="lg"
disabled={!methods.formState.isValid}
loading={isLoadingEditService}
>
Save
</Button>
</div>
</form>
</div>
</div>
</div>
</FormProvider>
</Section>
</div>
)
}

export default PageSettingsConfigureJobFeature
Loading
Loading