From 74eb8c601adae2a5ac389b0e41f559cc07e55bac Mon Sep 17 00:00:00 2001 From: bdebon Date: Thu, 22 Dec 2022 12:48:25 +0100 Subject: [PATCH] feat(jobs): cron and lifecycle job creation (#393) --- __tests__/utils/providers.tsx | 5 - __tests__/utils/wrap-with-react-hook-form.tsx | 7 +- libs/domains/application/src/index.ts | 1 + .../lib/mocks/factories/job-factory.mock.ts | 145 ++++++++++ .../src/lib/slices/applications.slice.ts | 18 +- .../secret-environment-variables.slice.ts | 2 + .../src/lib/slices/organization.slice.ts | 4 +- ...rud-environment-variable-modal-feature.tsx | 2 +- ...ort-environment-variable-modal-feature.tsx | 3 +- .../utils/form-check.tsx | 2 +- ...ge-settings-configure-job-feature.spec.tsx | 159 +++++++++++ .../page-settings-configure-job-feature.tsx | 160 +++++++++++ .../page-settings-feature.tsx | 99 ++++--- .../page-settings-general-feature.spec.tsx | 40 ++- .../page-settings-general-feature.tsx | 57 +++- .../application/src/lib/router/router.tsx | 6 + .../import-environment-variable-modal.tsx | 2 +- .../need-redeploy-flag/need-redeploy-flag.tsx | 3 +- .../page-settings-advanced.spec.tsx | 2 +- .../page-settings-configure-job.spec.tsx | 29 ++ .../page-settings-configure-job.tsx | 61 ++++ .../page-settings-general.tsx | 20 +- .../page-settings-resources.spec.tsx | 4 +- .../page-logout-feature.spec.tsx | 18 +- .../page-application-create-feature.tsx | 20 +- ...age-application-create-general-feature.tsx | 5 +- .../page-application-create-port-feature.tsx | 8 +- ...e-application-create-resources-feature.tsx | 5 +- .../page-job-create-feature.spec.tsx | 22 ++ .../page-job-create-feature.tsx | 127 +++++++++ .../step-configure-feature.spec.tsx | 32 +++ .../step-configure-feature.tsx | 122 ++++++++ .../step-general-feature.spec.tsx | 32 +++ .../step-general-feature.tsx | 78 +++++ .../step-resources-feature.spec.tsx | 32 +++ .../step-resources-feature.tsx | 82 ++++++ .../step-summary-feature.spec.tsx | 32 +++ .../step-summary-feature.tsx | 267 ++++++++++++++++++ .../step-variable-feature.spec.tsx | 32 +++ .../step-variable-feature.tsx | 96 +++++++ libs/pages/services/src/lib/router/router.tsx | 45 +++ .../src/lib/ui/container/container.tsx | 20 ++ .../page-application-create-general.tsx | 23 +- .../page-application-create-resources.tsx | 4 +- .../page-application-post.tsx | 12 +- .../step-configure/step-configure.spec.tsx | 65 +++++ .../step-configure/step-configure.tsx | 59 ++++ .../step-general/step-general.spec.tsx | 25 ++ .../step-general/step-general.tsx | 96 +++++++ .../step-resources/step-resources.spec.tsx | 25 ++ .../step-resources/step-resources.tsx | 50 ++++ .../step-summary/step-summary.spec.tsx | 43 +++ .../step-summary/step-summary.tsx | 240 ++++++++++++++++ libs/shared/console-shared/src/index.ts | 15 +- .../application-buttons-actions.spec.tsx | 0 .../{ => ui}/application-buttons-actions.tsx | 2 +- .../{ => ui}/cluster-buttons-actions.spec.tsx | 0 .../{ => ui}/cluster-buttons-actions.tsx | 0 .../create-general-git-application.spec.tsx | 0 .../ui}/create-general-git-application.tsx | 16 +- .../database-buttons-actions.spec.tsx | 0 .../{ => ui}/database-buttons-actions.tsx | 0 .../ui/entrypoint-cmd-inputs.spec.tsx | 13 + .../ui/entrypoint-cmd-inputs.tsx | 66 +++++ .../environment-buttons-actions.spec.tsx | 0 .../{ => ui}/environment-buttons-actions.tsx | 2 +- .../port-row/port-row.spec.tsx | 2 +- .../flow-create-port}/port-row/port-row.tsx | 4 +- .../ui/flow-create-port.spec.tsx} | 10 +- .../flow-create-port/ui/flow-create-port.tsx} | 14 +- .../ui/flow-create-variable.spec.tsx | 27 ++ .../ui/flow-create-variable.tsx | 82 ++++++ .../variable-row/variable-row.spec.tsx | 25 ++ .../variable-row/variable-row.tsx | 115 ++++++++ .../ui/general-container-settings.spec.tsx | 2 - .../ui/general-container-settings.tsx | 52 +--- .../auth-providers-values.tsx | 11 + ...-git-repository-settings-feature.spec.tsx} | 5 +- .../edit-git-repository-settings-feature.tsx} | 77 ++--- .../git-repository-settings-feature.tsx | 10 +- .../ui/git-repository-settings.spec.tsx | 2 +- .../ui/job-configure-settings.spec.tsx | 82 ++++++ .../ui/job-configure-settings.tsx | 207 ++++++++++++++ .../ui/job-general-settings.spec.tsx | 94 ++++++ .../ui/job-general-settings.tsx | 126 +++++++++ .../ui/setting-resources.tsx | 3 +- libs/shared/enums/src/index.ts | 1 + libs/shared/enums/src/lib/icon.enum.ts | 2 + libs/shared/enums/src/lib/job.type.ts | 3 + libs/shared/interfaces/src/index.ts | 4 + .../lib/common/flow-port-data.interface.ts | 9 + .../common/flow-variable-data.interface.ts | 12 + .../application-creation-flow.interface.ts | 12 +- .../lib/domain/job-creation-flow.interface.ts | 60 ++++ libs/shared/router/src/lib/router.ts | 1 + .../src/lib/sub-router/application.router.ts | 1 + .../router/src/lib/sub-router/job.router.ts | 10 + libs/shared/ui/src/index.ts | 1 + .../block-content/block-content.tsx | 11 +- .../components/enable-box/enable-box.spec.tsx | 68 +++++ .../enable-box/enable-box.stories.tsx | 31 ++ .../lib/components/enable-box/enable-box.tsx | 65 +++++ .../funnel-flow-body/funnel-flow-body.tsx | 2 +- .../src/lib/components/icon/icon.stories.tsx | 6 + .../ui/src/lib/components/icon/icon.tsx | 6 + .../icon/icons/cron-job-stroke-icon.tsx | 17 ++ .../icon/icons/lifecycle-job-stroke-icon.tsx | 44 +++ .../inputs/input-checkbox/input-checkbox.tsx | 12 +- .../layout-logs/tabs-logs/tabs-logs.tsx | 6 +- .../ui/src/lib/styles/components/input.scss | 2 +- libs/shared/utils/src/index.ts | 1 + ...ilable-environment-variable-scope.spec.tsx | 0 ...e-available-environment-variable-scope.tsx | 0 .../utils/src/lib/tools/refacto-payload.ts | 46 ++- package.json | 1 + yarn.lock | 129 +++++---- 116 files changed, 3869 insertions(+), 306 deletions(-) create mode 100644 libs/domains/application/src/lib/mocks/factories/job-factory.mock.ts create mode 100644 libs/pages/application/src/lib/feature/page-settings-configure-job-feature/page-settings-configure-job-feature.spec.tsx create mode 100644 libs/pages/application/src/lib/feature/page-settings-configure-job-feature/page-settings-configure-job-feature.tsx create mode 100644 libs/pages/application/src/lib/ui/page-settings-configure-job/page-settings-configure-job.spec.tsx create mode 100644 libs/pages/application/src/lib/ui/page-settings-configure-job/page-settings-configure-job.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/page-job-create-feature.spec.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/page-job-create-feature.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-configure-feature/step-configure-feature.spec.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-configure-feature/step-configure-feature.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-general-feature/step-general-feature.spec.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-general-feature/step-general-feature.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-resources-feature/step-resources-feature.spec.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-resources-feature/step-resources-feature.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-summary-feature/step-summary-feature.spec.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-summary-feature/step-summary-feature.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-variable-feature/step-variable-feature.spec.tsx create mode 100644 libs/pages/services/src/lib/feature/page-job-create-feature/step-variable-feature/step-variable-feature.tsx create mode 100644 libs/pages/services/src/lib/ui/page-job-create/step-configure/step-configure.spec.tsx create mode 100644 libs/pages/services/src/lib/ui/page-job-create/step-configure/step-configure.tsx create mode 100644 libs/pages/services/src/lib/ui/page-job-create/step-general/step-general.spec.tsx create mode 100644 libs/pages/services/src/lib/ui/page-job-create/step-general/step-general.tsx create mode 100644 libs/pages/services/src/lib/ui/page-job-create/step-resources/step-resources.spec.tsx create mode 100644 libs/pages/services/src/lib/ui/page-job-create/step-resources/step-resources.tsx create mode 100644 libs/pages/services/src/lib/ui/page-job-create/step-summary/step-summary.spec.tsx create mode 100644 libs/pages/services/src/lib/ui/page-job-create/step-summary/step-summary.tsx rename libs/shared/console-shared/src/lib/application-buttons-actions/{ => ui}/application-buttons-actions.spec.tsx (100%) rename libs/shared/console-shared/src/lib/application-buttons-actions/{ => ui}/application-buttons-actions.tsx (98%) rename libs/shared/console-shared/src/lib/cluster-buttons-actions/{ => ui}/cluster-buttons-actions.spec.tsx (100%) rename libs/shared/console-shared/src/lib/cluster-buttons-actions/{ => ui}/cluster-buttons-actions.tsx (100%) rename libs/{pages/services/src/lib/ui/page-application-create/page-application-create-general/create-general-git-application => shared/console-shared/src/lib/create-general-git-application/ui}/create-general-git-application.spec.tsx (100%) rename libs/{pages/services/src/lib/ui/page-application-create/page-application-create-general/create-general-git-application => shared/console-shared/src/lib/create-general-git-application/ui}/create-general-git-application.tsx (87%) rename libs/shared/console-shared/src/lib/database-buttons-actions/{ => ui}/database-buttons-actions.spec.tsx (100%) rename libs/shared/console-shared/src/lib/database-buttons-actions/{ => ui}/database-buttons-actions.tsx (100%) create mode 100644 libs/shared/console-shared/src/lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs.spec.tsx create mode 100644 libs/shared/console-shared/src/lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs.tsx rename libs/shared/console-shared/src/lib/environment-buttons-actions/{ => ui}/environment-buttons-actions.spec.tsx (100%) rename libs/shared/console-shared/src/lib/environment-buttons-actions/{ => ui}/environment-buttons-actions.tsx (98%) rename libs/{pages/services/src/lib/ui/page-application-create/page-application-create-port => shared/console-shared/src/lib/flow-create-port}/port-row/port-row.spec.tsx (92%) rename libs/{pages/services/src/lib/ui/page-application-create/page-application-create-port => shared/console-shared/src/lib/flow-create-port}/port-row/port-row.tsx (95%) rename libs/{pages/services/src/lib/ui/page-application-create/page-application-create-port/page-application-create-port.spec.tsx => shared/console-shared/src/lib/flow-create-port/ui/flow-create-port.spec.tsx} (68%) rename libs/{pages/services/src/lib/ui/page-application-create/page-application-create-port/page-application-create-port.tsx => shared/console-shared/src/lib/flow-create-port/ui/flow-create-port.tsx} (78%) create mode 100644 libs/shared/console-shared/src/lib/flow-create-variable/ui/flow-create-variable.spec.tsx create mode 100644 libs/shared/console-shared/src/lib/flow-create-variable/ui/flow-create-variable.tsx create mode 100644 libs/shared/console-shared/src/lib/flow-create-variable/variable-row/variable-row.spec.tsx create mode 100644 libs/shared/console-shared/src/lib/flow-create-variable/variable-row/variable-row.tsx create mode 100644 libs/shared/console-shared/src/lib/git-repository-settings/auth-providers-values.tsx rename libs/{pages/application/src/lib/feature/git-repository-settings-feature/git-repository-settings-feature.spec.tsx => shared/console-shared/src/lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature.spec.tsx} (75%) rename libs/{pages/application/src/lib/feature/git-repository-settings-feature/git-repository-settings-feature.tsx => shared/console-shared/src/lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature.tsx} (60%) create mode 100644 libs/shared/console-shared/src/lib/job-configure-settings/ui/job-configure-settings.spec.tsx create mode 100644 libs/shared/console-shared/src/lib/job-configure-settings/ui/job-configure-settings.tsx create mode 100644 libs/shared/console-shared/src/lib/job-general-settings/ui/job-general-settings.spec.tsx create mode 100644 libs/shared/console-shared/src/lib/job-general-settings/ui/job-general-settings.tsx create mode 100644 libs/shared/enums/src/lib/job.type.ts create mode 100644 libs/shared/interfaces/src/lib/common/flow-port-data.interface.ts create mode 100644 libs/shared/interfaces/src/lib/common/flow-variable-data.interface.ts rename libs/{pages/services/src/lib/feature/page-application-create-feature => shared/interfaces/src/lib/domain}/application-creation-flow.interface.ts (69%) create mode 100644 libs/shared/interfaces/src/lib/domain/job-creation-flow.interface.ts create mode 100644 libs/shared/router/src/lib/sub-router/job.router.ts create mode 100644 libs/shared/ui/src/lib/components/enable-box/enable-box.spec.tsx create mode 100644 libs/shared/ui/src/lib/components/enable-box/enable-box.stories.tsx create mode 100644 libs/shared/ui/src/lib/components/enable-box/enable-box.tsx create mode 100644 libs/shared/ui/src/lib/components/icon/icons/cron-job-stroke-icon.tsx create mode 100644 libs/shared/ui/src/lib/components/icon/icons/lifecycle-job-stroke-icon.tsx rename libs/{pages/application/src/lib/utils => shared/utils/src/lib/tools}/compute-available-environment-variable-scope.spec.tsx (100%) rename libs/{pages/application/src/lib/utils => shared/utils/src/lib/tools}/compute-available-environment-variable-scope.tsx (100%) diff --git a/__tests__/utils/providers.tsx b/__tests__/utils/providers.tsx index 1b39f6ed45..944182fd82 100644 --- a/__tests__/utils/providers.tsx +++ b/__tests__/utils/providers.tsx @@ -1,6 +1,5 @@ import { Auth0Provider } from '@auth0/auth0-react' import { configureStore } from '@reduxjs/toolkit' -import posthog from 'posthog-js' import React, { ComponentType, ReactNode } from 'react' import { Provider } from 'react-redux' import { MemoryRouter } from 'react-router-dom' @@ -21,10 +20,6 @@ export type Props = { export const Wrapper: React.FC = ({ children, reduxState = initialRootState(), route = '/' }) => { window.history.pushState({}, 'Test page', route) - posthog.init('__test__posthog__token', { - api_host: '__test__environment__posthog__apihost', - }) - const store = configureStore({ reducer: rootReducer, preloadedState: reduxState, diff --git a/__tests__/utils/wrap-with-react-hook-form.tsx b/__tests__/utils/wrap-with-react-hook-form.tsx index ea9a57086c..d73f6cbfb9 100644 --- a/__tests__/utils/wrap-with-react-hook-form.tsx +++ b/__tests__/utils/wrap-with-react-hook-form.tsx @@ -1,5 +1,5 @@ -import { FormProvider, useForm } from 'react-hook-form' import React from 'react' +import { FormProvider, useForm } from 'react-hook-form' /** * Testing Library utility function to wrap tested component in React Hook Form @@ -8,7 +8,10 @@ import React from 'react' * @param {Object} objectParameters.defaultValues Initial form values to pass into * React Hook Form, which you can then assert against */ -export function wrapWithReactHookForm(ui: React.ReactElement, { defaultValues = {} } = {}) { +export function wrapWithReactHookForm( + ui: React.ReactElement, + { defaultValues }: { defaultValues?: T } = {} +) { const Wrapper = ({ children }: { children: React.ReactElement }) => { const methods = useForm({ defaultValues, mode: 'all' }) return {children} diff --git a/libs/domains/application/src/index.ts b/libs/domains/application/src/index.ts index 37f1708a95..372537665b 100644 --- a/libs/domains/application/src/index.ts +++ b/libs/domains/application/src/index.ts @@ -1,5 +1,6 @@ export * from './lib/slices/custom-domain.slice' export * from './lib/mocks/factories/application-factory.mock' +export * from './lib/mocks/factories/job-factory.mock' export * from './lib/mocks/factories/application-deployment-factory.mock' export * from './lib/slices/applications.slice' export * from './lib/slices/application.actions' diff --git a/libs/domains/application/src/lib/mocks/factories/job-factory.mock.ts b/libs/domains/application/src/lib/mocks/factories/job-factory.mock.ts new file mode 100644 index 0000000000..0889d16184 --- /dev/null +++ b/libs/domains/application/src/lib/mocks/factories/job-factory.mock.ts @@ -0,0 +1,145 @@ +import { Chance } from 'chance' +import { GitProviderEnum, StorageTypeEnum } from 'qovery-typescript-axios' +import { JobApplicationEntity } from '@qovery/shared/interfaces' + +const chance = new Chance() + +export const cronjobFactoryMock = (howMany: number, withContainer: boolean = false): JobApplicationEntity[] => + Array.from({ length: howMany }).map((_, index) => { + let source + if (!withContainer) { + source = { + docker: { + git_repository: { + has_access: true, + deployed_commit_id: '8a21ddb195d821781d46eb1d8f26d5ae13609dd1', + deployed_commit_date: '2022-12-19T11:57:07.425715Z', + deployed_commit_contributor: 'TAGS_NOT_IMPLEMENTED', + deployed_commit_tag: 'TAGS_NOT_IMPLEMENTED', + provider: GitProviderEnum.GITHUB, + owner: 'bdebon', + url: 'https://github.com/Qovery/admin-ui.git', + name: 'Qovery/admin-ui', + branch: 'develop', + root_path: '/', + }, + dockerfile_path: 'Dockerfile', + }, + } + } else { + source = { + image: { + name: 'nginx', + image_name: 'nginx/nginx', + registry_id: chance.guid(), + }, + } + } + + return { + id: `${index}`, + created_at: new Date().toString(), + updated_at: new Date().toString(), + storage: [ + { + id: chance.guid(), + type: chance.pickone(Object.values([StorageTypeEnum.FAST_SSD])), + size: 10, + mount_point: '', + }, + ], + environment: { + id: chance.guid(), + }, + maximum_cpu: 10, + maximum_memory: 10, + name: chance.name(), + description: chance.word({ length: 10 }), + max_duration_seconds: 10, + max_nb_restart: 10, + port: 80, + cpu: 1000, + memory: 1024, + auto_preview: false, + source, + schedule: { + cronjob: { + scheduled_at: '0 0 * * *', + entrypoint: '/', + arguments: [], + }, + }, + registry: { + id: chance.guid(), + }, + } + }) + +export const lifecycleJobFactoryMock = (howMany: number, withContainer: boolean = false): JobApplicationEntity[] => + Array.from({ length: howMany }).map((_, index) => { + let source + if (!withContainer) { + source = { + docker: { + git_repository: { + has_access: true, + deployed_commit_id: '8a21ddb195d821781d46eb1d8f26d5ae13609dd1', + deployed_commit_date: '2022-12-19T11:57:07.425715Z', + deployed_commit_contributor: 'TAGS_NOT_IMPLEMENTED', + deployed_commit_tag: 'TAGS_NOT_IMPLEMENTED', + provider: GitProviderEnum.GITHUB, + owner: 'bdebon', + url: 'https://github.com/Qovery/admin-ui.git', + name: 'Qovery/admin-ui', + branch: 'develop', + root_path: '/', + }, + dockerfile_path: 'Dockerfile', + }, + } + } else { + source = { + image: { + name: 'nginx', + image_name: 'nginx/nginx', + registry_id: chance.guid(), + }, + } + } + + return { + id: `${index}`, + created_at: new Date().toString(), + updated_at: new Date().toString(), + storage: [ + { + id: chance.guid(), + type: chance.pickone(Object.values([StorageTypeEnum.FAST_SSD])), + size: 10, + mount_point: '', + }, + ], + environment: { + id: chance.guid(), + }, + maximum_cpu: 10, + maximum_memory: 10, + name: chance.name(), + description: chance.word({ length: 10 }), + max_duration_seconds: 10, + max_nb_restart: 10, + port: 80, + cpu: 1000, + memory: 1024, + auto_preview: false, + source, + schedule: { + on_start: { + arguments: [], + }, + }, + registry: { + id: chance.guid(), + }, + } + }) diff --git a/libs/domains/application/src/lib/slices/applications.slice.ts b/libs/domains/application/src/lib/slices/applications.slice.ts index 47b7c3e07e..291eb6311c 100644 --- a/libs/domains/application/src/lib/slices/applications.slice.ts +++ b/libs/domains/application/src/lib/slices/applications.slice.ts @@ -29,11 +29,13 @@ import { Instance, JobDeploymentHistoryApi, JobMainCallsApi, + JobRequest, + JobResponse, JobsApi, Link, Status, } from 'qovery-typescript-axios' -import { ServiceTypeEnum, isContainer, isJob } from '@qovery/shared/enums' +import { ServiceTypeEnum, isApplication, isContainer, isJob } from '@qovery/shared/enums' import { ApplicationEntity, ApplicationsState, @@ -49,6 +51,7 @@ import { getEntitiesByIds, refactoContainerApplicationPayload, refactoGitApplicationPayload, + refactoJobPayload, shortToLongId, } from '@qovery/shared/utils' import { RootState } from '@qovery/store' @@ -130,6 +133,9 @@ export const editApplication = createAsyncThunk( if (isContainer(payload.serviceType)) { const cloneApplication = Object.assign({}, refactoContainerApplicationPayload(payload.data)) response = await containerMainCallsApi.editContainer(payload.applicationId, cloneApplication as ContainerRequest) + } else if (isJob(payload.serviceType)) { + const cloneJob = Object.assign({}, refactoJobPayload(payload.data as Partial)) + response = await jobMainCallsApi.editJob(payload.applicationId, cloneJob as JobRequest) } else { const cloneApplication = Object.assign({}, refactoGitApplicationPayload(payload.data)) response = await applicationMainCallsApi.editApplication( @@ -146,17 +152,19 @@ export const createApplication = createAsyncThunk( 'application/create', async (payload: { environmentId: string - data: ApplicationRequest | ContainerRequest + data: ApplicationRequest | ContainerRequest | JobRequest serviceType: ServiceTypeEnum }) => { let response if (isContainer(payload.serviceType)) { response = await containersApi.createContainer(payload.environmentId, payload.data as ContainerRequest) - } else { + } else if (isApplication(payload.serviceType)) { response = await applicationsApi.createApplication(payload.environmentId, payload.data as ApplicationRequest) + } else { + response = await jobsApi.createJob(payload.environmentId, payload.data as JobRequest) } - return response.data as Application | ContainerResponse + return response.data as Application | ContainerResponse | JobResponse } ) @@ -363,7 +371,7 @@ export const applicationsSlice = createSlice({ if (!action.meta.arg.silentToaster) { toast( ToastEnum.SUCCESS, - `Application updated`, + `${isJob(action.payload) ? 'Job' : 'Application'} updated`, 'You must redeploy to apply the settings update', action.meta.arg.toasterCallback, undefined, diff --git a/libs/domains/environment-variable/src/lib/slices/secret-environment-variables.slice.ts b/libs/domains/environment-variable/src/lib/slices/secret-environment-variables.slice.ts index ace98ed899..36e1ae51fa 100644 --- a/libs/domains/environment-variable/src/lib/slices/secret-environment-variables.slice.ts +++ b/libs/domains/environment-variable/src/lib/slices/secret-environment-variables.slice.ts @@ -32,6 +32,8 @@ export const fetchSecretEnvironmentVariables = createAsyncThunk( let response if (isContainer(payload.serviceType)) { response = await containerSecretApi.listContainerSecrets(payload.applicationId) + } else if (isJob(payload.serviceType)) { + response = await jobSecretApi.listJobSecrets(payload.applicationId) } else { response = await applicationSecretApi.listApplicationSecrets(payload.applicationId) } diff --git a/libs/domains/organization/src/lib/slices/organization.slice.ts b/libs/domains/organization/src/lib/slices/organization.slice.ts index e7a780e6b2..38cd0acef3 100644 --- a/libs/domains/organization/src/lib/slices/organization.slice.ts +++ b/libs/domains/organization/src/lib/slices/organization.slice.ts @@ -235,7 +235,7 @@ export const organizationSlice = createSlice({ state.loadingStatus = 'loading' }) .addCase(fetchOrganization.fulfilled, (state: OrganizationState, action: PayloadAction) => { - organizationAdapter.setAll(state, action.payload) + organizationAdapter.upsertMany(state, action.payload) state.loadingStatus = 'loaded' }) .addCase(fetchOrganization.rejected, (state: OrganizationState, action) => { @@ -246,7 +246,7 @@ export const organizationSlice = createSlice({ .addCase( fetchOrganizationById.fulfilled, (state: OrganizationState, action: PayloadAction) => { - organizationAdapter.addOne(state, action.payload) + organizationAdapter.upsertOne(state, action.payload) state.loadingStatus = 'loaded' } ) diff --git a/libs/pages/application/src/lib/feature/crud-environment-variable-modal-feature/crud-environment-variable-modal-feature.tsx b/libs/pages/application/src/lib/feature/crud-environment-variable-modal-feature/crud-environment-variable-modal-feature.tsx index d9e09373a2..c0ae86132d 100644 --- a/libs/pages/application/src/lib/feature/crud-environment-variable-modal-feature/crud-environment-variable-modal-feature.tsx +++ b/libs/pages/application/src/lib/feature/crud-environment-variable-modal-feature/crud-environment-variable-modal-feature.tsx @@ -6,9 +6,9 @@ import { getEnvironmentVariablesState } from '@qovery/domains/environment-variab import { ServiceTypeEnum } from '@qovery/shared/enums' import { EnvironmentVariableEntity, EnvironmentVariableSecretOrPublic } from '@qovery/shared/interfaces' import { useModal } from '@qovery/shared/ui' +import { computeAvailableScope } from '@qovery/shared/utils' import { AppDispatch, RootState } from '@qovery/store' import CrudEnvironmentVariableModal from '../../ui/crud-environment-variable-modal/crud-environment-variable-modal' -import { computeAvailableScope } from '../../utils/compute-available-environment-variable-scope' import { handleSubmitForEnvSecretCreation } from './handle-submit/handle-submit' export interface CrudEnvironmentVariableModalFeatureProps { diff --git a/libs/pages/application/src/lib/feature/import-environment-variable-modal-feature/import-environment-variable-modal-feature.tsx b/libs/pages/application/src/lib/feature/import-environment-variable-modal-feature/import-environment-variable-modal-feature.tsx index 5a18c48431..23ceb19d3e 100644 --- a/libs/pages/application/src/lib/feature/import-environment-variable-modal-feature/import-environment-variable-modal-feature.tsx +++ b/libs/pages/application/src/lib/feature/import-environment-variable-modal-feature/import-environment-variable-modal-feature.tsx @@ -16,10 +16,9 @@ import { SecretEnvironmentVariableEntity, } from '@qovery/shared/interfaces' import { useModal } from '@qovery/shared/ui' -import { parseEnvText } from '@qovery/shared/utils' +import { computeAvailableScope, parseEnvText } from '@qovery/shared/utils' import { AppDispatch, RootState } from '@qovery/store' import ImportEnvironmentVariableModal from '../../ui/import-environment-variable-modal/import-environment-variable-modal' -import { computeAvailableScope } from '../../utils/compute-available-environment-variable-scope' import { changeScopeForAll } from './utils/change-scope-all' import { deleteEntry } from './utils/delete-entry' import { parsedToForm } from './utils/file-to-form' diff --git a/libs/pages/application/src/lib/feature/import-environment-variable-modal-feature/utils/form-check.tsx b/libs/pages/application/src/lib/feature/import-environment-variable-modal-feature/utils/form-check.tsx index 8e4dd963fa..5514f588ec 100644 --- a/libs/pages/application/src/lib/feature/import-environment-variable-modal-feature/utils/form-check.tsx +++ b/libs/pages/application/src/lib/feature/import-environment-variable-modal-feature/utils/form-check.tsx @@ -1,7 +1,7 @@ import { APIVariableScopeEnum } from 'qovery-typescript-axios' import { ServiceTypeEnum } from '@qovery/shared/enums' import { EnvironmentVariableSecretOrPublic } from '@qovery/shared/interfaces' -import { getScopeHierarchy } from '../../../utils/compute-available-environment-variable-scope' +import { getScopeHierarchy } from '@qovery/shared/utils' export const validateKey = ( value: string, diff --git a/libs/pages/application/src/lib/feature/page-settings-configure-job-feature/page-settings-configure-job-feature.spec.tsx b/libs/pages/application/src/lib/feature/page-settings-configure-job-feature/page-settings-configure-job-feature.spec.tsx new file mode 100644 index 0000000000..9dbda85172 --- /dev/null +++ b/libs/pages/application/src/lib/feature/page-settings-configure-job-feature/page-settings-configure-job-feature.spec.tsx @@ -0,0 +1,159 @@ +import { act, fireEvent, getAllByLabelText, getByLabelText, getByTestId, waitFor } from '@testing-library/react' +import { render } from '__tests__/utils/setup-jest' +import * as storeApplication from '@qovery/domains/application' +import { cronjobFactoryMock, lifecycleJobFactoryMock } from '@qovery/domains/application' +import PageSettingsConfigureJobFeature from './page-settings-configure-job-feature' + +import SpyInstance = jest.SpyInstance + +const mockJobApplication = cronjobFactoryMock(1)[0] +const mockSelectApplicationById = jest.fn() +jest.mock('@qovery/domains/application', () => { + return { + ...jest.requireActual('@qovery/domains/application'), + editApplication: jest.fn(), + getApplicationsState: () => ({ + loadingStatus: 'loaded', + ids: [mockJobApplication.id], + entities: { + [mockJobApplication.id]: { mockJobApplication }, + }, + error: null, + }), + selectApplicationById: () => mockSelectApplicationById(), + } +}) + +const mockDispatch = jest.fn() +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})) + +describe('PageSettingsPortsFeature', () => { + it('should render successfully', () => { + const { baseElement } = render() + expect(baseElement).toBeTruthy() + }) + + describe('when job is CRON', () => { + it('should call edit application with correct payload', async () => { + const editApplicationSpy: SpyInstance = jest.spyOn(storeApplication, 'editApplication') + mockDispatch.mockImplementation(() => ({ + unwrap: () => + Promise.resolve({ + data: {}, + }), + })) + + mockSelectApplicationById.mockImplementation(() => ({ + ...mockJobApplication, + })) + + const { baseElement } = render() + + await act(() => { + fireEvent.change(getByLabelText(baseElement, 'Schedule - Cron expression'), { + target: { value: '9 * * * *' }, + }) + fireEvent.change(getByLabelText(baseElement, 'Image Entrypoint'), { target: { value: 'some new text value' } }) + fireEvent.change(getByLabelText(baseElement, 'CMD Arguments'), { target: { value: "['string']" } }) + fireEvent.change(getByLabelText(baseElement, 'Number of restarts'), { target: { value: 12 } }) + fireEvent.change(getByLabelText(baseElement, 'Max duration in seconds'), { target: { value: 123 } }) + fireEvent.change(getByLabelText(baseElement, 'Port'), { target: { value: 123 } }) + }) + + const submitButton = getByTestId(baseElement, 'submit-button') + + await act(() => { + submitButton.click() + }) + + expect(editApplicationSpy.mock.calls[0][0].data).toStrictEqual({ + ...mockJobApplication, + schedule: { + cronjob: { + arguments: ['string'], + entrypoint: 'some new text value', + scheduled_at: '9 * * * *', + }, + }, + max_duration_seconds: '123', + max_nb_restart: '12', + port: '123', + }) + }) + }) + + describe('when job is Lifecycle', () => { + it('should call edit application with correct payload', async () => { + const editApplicationSpy: SpyInstance = jest.spyOn(storeApplication, 'editApplication') + mockDispatch.mockImplementation(() => ({ + unwrap: () => + Promise.resolve({ + data: {}, + }), + })) + + const mockLifecycleJobApplication = lifecycleJobFactoryMock(1)[0] + mockSelectApplicationById.mockImplementation(() => ({ + ...mockLifecycleJobApplication, + })) + + const { baseElement } = render() + + let checkbox = getByLabelText(baseElement, 'Start') + await act(() => { + checkbox.click() + }) + + checkbox = getByLabelText(baseElement, 'Delete') + await act(() => { + checkbox.click() + }) + let entrypoints: HTMLElement[] + let cmds: HTMLElement[] + + await waitFor(() => { + entrypoints = getAllByLabelText(baseElement, 'Image Entrypoint') + cmds = getAllByLabelText(baseElement, 'CMD Arguments') + + // must have close the Start inputs and open the Delete inputs + // because we fetch the inputs by label and there are two elements with the same label on the page + // which create a bug + expect(entrypoints.length).toBe(1) + expect(cmds.length).toBe(1) + }) + + await act(() => { + fireEvent.change(entrypoints[0], { target: { value: '/' } }) + fireEvent.change(cmds[0], { target: { value: '["string"]' } }) + }) + + await act(() => { + fireEvent.change(getByLabelText(baseElement, 'Number of restarts'), { target: { value: 12 } }) + fireEvent.change(getByLabelText(baseElement, 'Max duration in seconds'), { target: { value: 123 } }) + fireEvent.change(getByLabelText(baseElement, 'Port'), { target: { value: 123 } }) + }) + + const submitButton = getByTestId(baseElement, 'submit-button') + + await act(() => { + submitButton.click() + }) + + expect(editApplicationSpy.mock.calls[0][0].data).toStrictEqual({ + ...mockLifecycleJobApplication, + schedule: { + on_delete: { + arguments: ['string'], + entrypoint: '/', + }, + }, + max_duration_seconds: '123', + max_nb_restart: '12', + port: '123', + }) + }) + }) +}) diff --git a/libs/pages/application/src/lib/feature/page-settings-configure-job-feature/page-settings-configure-job-feature.tsx b/libs/pages/application/src/lib/feature/page-settings-configure-job-feature/page-settings-configure-job-feature.tsx new file mode 100644 index 0000000000..65715b7ee7 --- /dev/null +++ b/libs/pages/application/src/lib/feature/page-settings-configure-job-feature/page-settings-configure-job-feature.tsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { editApplication, postApplicationActionsRestart, selectApplicationById } from '@qovery/domains/application' +import { ServiceTypeEnum, getServiceType, isCronJob, isLifeCycleJob } from '@qovery/shared/enums' +import { ApplicationEntity, JobApplicationEntity, JobConfigureData } from '@qovery/shared/interfaces' +import { toastError } from '@qovery/shared/toast' +import { AppDispatch, RootState } from '@qovery/store' +import PageSettingsConfigureJob from '../../ui/page-settings-configure-job/page-settings-configure-job' + +export function PageSettingsConfigureJobFeature() { + const { applicationId = '', environmentId = '' } = useParams() + const methods = useForm() + + const application: JobApplicationEntity | undefined = useSelector( + (state) => selectApplicationById(state, applicationId), + (a, b) => { + return JSON.stringify(a?.id) === JSON.stringify(b?.id) + } + ) as JobApplicationEntity | undefined + + const [loading, setLoading] = useState(false) + + const dispatch = useDispatch() + + const toasterCallback = () => { + if (application) { + dispatch( + postApplicationActionsRestart({ applicationId, environmentId, serviceType: getServiceType(application) }) + ) + } + } + + useEffect(() => { + if (application) { + methods.setValue('max_duration', application.max_duration_seconds) + methods.setValue('nb_restarts', application.max_nb_restart) + methods.setValue('port', application.port || undefined) + + if (isCronJob(application)) { + methods.setValue('schedule', application.schedule?.cronjob?.scheduled_at || undefined) + methods.setValue('cmd_arguments', JSON.stringify(application.schedule?.cronjob?.arguments) || undefined) + methods.setValue('image_entry_point', application.schedule?.cronjob?.entrypoint || undefined) + } else { + methods.setValue('on_start.enabled', !!application.schedule?.on_start) + methods.setValue('on_start.arguments_string', JSON.stringify(application.schedule?.on_start?.arguments)) + methods.setValue('on_start.entrypoint', application.schedule?.on_start?.entrypoint) + + methods.setValue('on_stop.enabled', !!application.schedule?.on_stop) + methods.setValue('on_stop.arguments_string', JSON.stringify(application.schedule?.on_stop?.arguments)) + methods.setValue('on_stop.entrypoint', application.schedule?.on_stop?.entrypoint) + + methods.setValue('on_delete.enabled', !!application.schedule?.on_delete) + methods.setValue('on_delete.arguments_string', JSON.stringify(application.schedule?.on_delete?.arguments)) + methods.setValue('on_delete.entrypoint', application.schedule?.on_delete?.entrypoint) + } + } + }, [application, methods]) + + const onSubmit = methods.handleSubmit((data) => { + setLoading(true) + const job = { ...application } + + job.max_duration_seconds = data.max_duration + job.max_nb_restart = data.nb_restarts + job.port = data.port + + if (isCronJob(application)) { + const schedule: any = {} + if (job.schedule?.cronjob) { + schedule.cronjob = { + scheduled_at: data.schedule || '', + } + + if (data.cmd_arguments) { + try { + schedule.cronjob.arguments = eval(data.cmd_arguments) + } catch (e: any) { + toastError(e, 'Invalid CMD array') + return + } + } + schedule.cronjob.entrypoint = data.image_entry_point + } + job.schedule = schedule + } + + if (isLifeCycleJob(application)) { + const schedule: any = {} + if (data.on_start?.enabled) { + schedule.on_start = { + entrypoint: data.on_start.entrypoint, + } + + try { + if (data.on_start?.arguments_string) { + schedule.on_start.arguments = eval(data.on_start.arguments_string) + } + } catch (e: any) { + toastError(e, 'Invalid CMD array') + return + } + } + + if (data.on_stop?.enabled) { + schedule.on_stop = { + entrypoint: data.on_stop.entrypoint, + } + + try { + if (data.on_stop?.arguments_string) { + schedule.on_stop.arguments = eval(data.on_stop.arguments_string) + } + } catch (e: any) { + toastError(e, 'Invalid CMD array') + return + } + } + + if (data.on_delete?.enabled) { + schedule.on_delete = { + entrypoint: data.on_delete.entrypoint, + } + + try { + if (data.on_delete?.arguments_string) { + schedule.on_delete.arguments = eval(data.on_delete.arguments_string) + } + } catch (e: any) { + toastError(e, 'Invalid CMD array') + return + } + } + + job.schedule = schedule + } + + dispatch( + editApplication({ + data: job, + applicationId: job.id as string, + serviceType: ServiceTypeEnum.JOB, + toasterCallback, + }) + ) + .unwrap() + .then(() => {}) + .finally(() => setLoading(false)) + .catch((e) => console.error(e)) + }) + + return ( + + + + ) +} + +export default PageSettingsConfigureJobFeature diff --git a/libs/pages/application/src/lib/feature/page-settings-feature/page-settings-feature.tsx b/libs/pages/application/src/lib/feature/page-settings-feature/page-settings-feature.tsx index ebc35b400d..05f550fb8d 100644 --- a/libs/pages/application/src/lib/feature/page-settings-feature/page-settings-feature.tsx +++ b/libs/pages/application/src/lib/feature/page-settings-feature/page-settings-feature.tsx @@ -1,6 +1,12 @@ +import { useCallback } from 'react' +import { useSelector } from 'react-redux' import { Navigate, Route, Routes, useParams } from 'react-router-dom' +import { getApplicationsState } from '@qovery/domains/application' +import { isJob } from '@qovery/shared/enums' +import { ApplicationEntity } from '@qovery/shared/interfaces' import { APPLICATION_SETTINGS_ADVANCED_SETTINGS_URL, + APPLICATION_SETTINGS_CONFIGURE_URL, APPLICATION_SETTINGS_DANGER_ZONE_URL, APPLICATION_SETTINGS_DOMAIN_URL, APPLICATION_SETTINGS_GENERAL_URL, @@ -10,7 +16,9 @@ import { APPLICATION_SETTINGS_URL, APPLICATION_URL, } from '@qovery/shared/router' +import { IconAwesomeEnum } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/utils' +import { RootState } from '@qovery/store' import { ROUTER_APPLICATION_SETTINGS } from '../../router/router' import PageSettings from '../../ui/page-settings/page-settings' @@ -26,46 +34,69 @@ export function PageSettingsFeature() { applicationId )}${APPLICATION_SETTINGS_URL}` - const links = [ - { - title: 'General', - icon: 'icon-solid-wheel', - url: pathSettings + APPLICATION_SETTINGS_GENERAL_URL, - }, - { + const application = useSelector( + (state) => getApplicationsState(state).entities[applicationId] + ) + + const getLinks = useCallback(() => { + const links = [ + { + title: 'General', + icon: IconAwesomeEnum.WHEEL, + url: pathSettings + APPLICATION_SETTINGS_GENERAL_URL, + }, + ] + + if (isJob(application)) { + links.push({ + title: 'Configure Job', + icon: IconAwesomeEnum.GEARS, + url: pathSettings + APPLICATION_SETTINGS_CONFIGURE_URL, + }) + } + + links.push({ title: 'Resources', - icon: 'icon-solid-chart-bullet', + icon: IconAwesomeEnum.CHART_BULLET, url: pathSettings + APPLICATION_SETTINGS_RESOURCES_URL, - }, - { - title: 'Storage', - icon: 'icon-solid-hard-drive', - url: pathSettings + APPLICATION_SETTINGS_STORAGE_URL, - }, - { - title: 'Domain', - icon: 'icon-solid-earth-americas', - url: pathSettings + APPLICATION_SETTINGS_DOMAIN_URL, - }, - { - title: 'Port', - icon: 'icon-solid-plug', - url: pathSettings + APPLICATION_SETTINGS_PORT_URL, - }, - { - title: 'Advanced settings', - icon: 'icon-solid-gears', - url: pathSettings + APPLICATION_SETTINGS_ADVANCED_SETTINGS_URL, - }, - { + }) + + if (!isJob(application)) { + links.push( + { + title: 'Storage', + icon: IconAwesomeEnum.HARD_DRIVE, + url: pathSettings + APPLICATION_SETTINGS_STORAGE_URL, + }, + { + title: 'Domain', + icon: IconAwesomeEnum.EARTH_AMERICAS, + url: pathSettings + APPLICATION_SETTINGS_DOMAIN_URL, + }, + { + title: 'Port', + icon: IconAwesomeEnum.PLUG, + url: pathSettings + APPLICATION_SETTINGS_PORT_URL, + }, + { + title: 'Advanced settings', + icon: IconAwesomeEnum.GEARS, + url: pathSettings + APPLICATION_SETTINGS_ADVANCED_SETTINGS_URL, + } + ) + } + + links.push({ title: 'Danger zone', - icon: 'icon-solid-skull', + icon: IconAwesomeEnum.SKULL, url: pathSettings + APPLICATION_SETTINGS_DANGER_ZONE_URL, - }, - ] + }) + + return links + }, [application, pathSettings]) return ( - + {ROUTER_APPLICATION_SETTINGS.map((route) => ( diff --git a/libs/pages/application/src/lib/feature/page-settings-general-feature/page-settings-general-feature.spec.tsx b/libs/pages/application/src/lib/feature/page-settings-general-feature/page-settings-general-feature.spec.tsx index ca64959050..79d8308307 100644 --- a/libs/pages/application/src/lib/feature/page-settings-general-feature/page-settings-general-feature.spec.tsx +++ b/libs/pages/application/src/lib/feature/page-settings-general-feature/page-settings-general-feature.spec.tsx @@ -1,8 +1,8 @@ import { render } from '__tests__/utils/setup-jest' import { BuildModeEnum, BuildPackLanguageEnum, GitProviderEnum } from 'qovery-typescript-axios' -import { applicationFactoryMock } from '@qovery/domains/application' +import { applicationFactoryMock, cronjobFactoryMock } from '@qovery/domains/application' import { ApplicationEntity } from '@qovery/shared/interfaces' -import PageSettingsGeneralFeature, { handleSubmit } from './page-settings-general-feature' +import PageSettingsGeneralFeature, { handleJobSubmit, handleSubmit } from './page-settings-general-feature' describe('PageSettingsGeneralFeature', () => { let application: ApplicationEntity @@ -74,4 +74,40 @@ describe('PageSettingsGeneralFeature', () => { expect(app.git_repository?.root_path).toBe('/') expect(app.git_repository?.url).toBe('https://github.com/qovery/console.git') }) + + it('should update the job with git repository', () => { + const job = cronjobFactoryMock(1)[0] + const app = handleJobSubmit( + { + name: 'hello', + dockerfile_path: '/', + provider: GitProviderEnum.GITHUB, + repository: 'qovery/console', + branch: 'main', + root_path: '/', + }, + job + ) + + expect(app.source?.docker?.git_repository?.branch).toBe('main') + expect(app.source?.docker?.git_repository?.root_path).toBe('/') + expect(app.source?.docker?.git_repository?.url).toBe('https://github.com/qovery/console.git') + }) + + it('should update the job with image', () => { + const job = cronjobFactoryMock(1, true)[0] + const app = handleJobSubmit( + { + name: 'hello', + image_tag: 'latest', + image_name: 'qovery/console', + registry: 'docker.io', + }, + job + ) + + expect(app.source?.image?.tag).toBe('latest') + expect(app.source?.image?.image_name).toBe('qovery/console') + expect(app.source?.image?.registry_id).toBe('docker.io') + }) }) diff --git a/libs/pages/application/src/lib/feature/page-settings-general-feature/page-settings-general-feature.tsx b/libs/pages/application/src/lib/feature/page-settings-general-feature/page-settings-general-feature.tsx index 5234c931da..fbfce29558 100644 --- a/libs/pages/application/src/lib/feature/page-settings-general-feature/page-settings-general-feature.tsx +++ b/libs/pages/application/src/lib/feature/page-settings-general-feature/page-settings-general-feature.tsx @@ -5,11 +5,12 @@ import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { editApplication, getApplicationsState, postApplicationActionsRestart } from '@qovery/domains/application' import { fetchOrganizationContainerRegistries, selectOrganizationById } from '@qovery/domains/organization' -import { getServiceType, isApplication, isContainer } from '@qovery/shared/enums' +import { ServiceTypeEnum, getServiceType, isApplication, isContainer, isJob } from '@qovery/shared/enums' import { ApplicationEntity, ContainerApplicationEntity, GitApplicationEntity, + JobApplicationEntity, OrganizationEntity, } from '@qovery/shared/interfaces' import { toastError } from '@qovery/shared/toast' @@ -56,6 +57,38 @@ export const handleContainerSubmit = (data: FieldValues, application: Applicatio } } +export const handleJobSubmit = (data: FieldValues, application: ApplicationEntity): JobApplicationEntity => { + if ((application as JobApplicationEntity).source?.docker) { + const git_repository = { + url: buildGitRepoUrl(data['provider'], data['repository']), + branch: data['branch'], + root_path: data['root_path'], + } + + return { + ...(application as JobApplicationEntity), + name: data['name'], + source: { + docker: { + git_repository, + dockerfile_path: data['dockerfile_path'], + }, + }, + } + } else { + return { + ...(application as JobApplicationEntity), + source: { + image: { + tag: data['image_tag'] || '', + image_name: data['image_name'] || '', + registry_id: data['registry'] || '', + }, + }, + } + } +} + export function PageSettingsGeneralFeature() { const { applicationId = '', environmentId = '', organizationId = '' } = useParams() const dispatch = useDispatch() @@ -91,6 +124,8 @@ export function PageSettingsGeneralFeature() { let cloneApplication: ApplicationEntity if (isApplication(application)) { cloneApplication = handleSubmit(data, application) + } else if (isJob(application)) { + cloneApplication = handleJobSubmit(data, application) } else { try { cloneApplication = handleContainerSubmit(data, application) @@ -172,6 +207,26 @@ export function PageSettingsGeneralFeature() { dispatch(fetchOrganizationContainerRegistries({ organizationId })) } } + + if (isJob(application)) { + methods.setValue('description', (application as JobApplicationEntity).description) + + const serviceType = (application as JobApplicationEntity).source?.docker + ? ServiceTypeEnum.APPLICATION + : ServiceTypeEnum.CONTAINER + methods.setValue('serviceType', serviceType) + + if (serviceType === ServiceTypeEnum.CONTAINER) { + dispatch(fetchOrganizationContainerRegistries({ organizationId })) + + methods.setValue('registry', (application as JobApplicationEntity).source?.image?.registry_id) + methods.setValue('image_name', (application as JobApplicationEntity).source?.image?.image_name) + methods.setValue('image_tag', (application as JobApplicationEntity).source?.image?.tag) + } else { + methods.setValue('build_mode', BuildModeEnum.DOCKER) + methods.setValue('dockerfile_path', (application as JobApplicationEntity).source?.docker?.dockerfile_path) + } + } }, [methods, application, dispatch, organizationId]) return ( diff --git a/libs/pages/application/src/lib/router/router.tsx b/libs/pages/application/src/lib/router/router.tsx index 858cd9358c..ea45317ca4 100644 --- a/libs/pages/application/src/lib/router/router.tsx +++ b/libs/pages/application/src/lib/router/router.tsx @@ -3,6 +3,7 @@ import { APPLICATION_GENERAL_URL, APPLICATION_METRICS_URL, APPLICATION_SETTINGS_ADVANCED_SETTINGS_URL, + APPLICATION_SETTINGS_CONFIGURE_URL, APPLICATION_SETTINGS_DANGER_ZONE_URL, APPLICATION_SETTINGS_DOMAIN_URL, APPLICATION_SETTINGS_GENERAL_URL, @@ -17,6 +18,7 @@ import PageDeploymentsFeature from '../feature/page-deployments-feature/page-dep import PageGeneralFeature from '../feature/page-general-feature/page-general-feature' import PageMetricsFeature from '../feature/page-metrics-feature/page-metrics-feature' import PageSettingsAdvancedFeature from '../feature/page-settings-advanced-feature/page-settings-advanced-feature' +import PageSettingsConfigureJobFeature from '../feature/page-settings-configure-job-feature/page-settings-configure-job-feature' import PageSettingsDangerZoneFeature from '../feature/page-settings-danger-zone-feature/page-settings-danger-zone-feature' import PageSettingsDomainsFeature from '../feature/page-settings-domains-feature/page-settings-domains-feature' import PageSettingsFeature from '../feature/page-settings-feature/page-settings-feature' @@ -54,6 +56,10 @@ export const ROUTER_APPLICATION_SETTINGS: Route[] = [ path: APPLICATION_SETTINGS_GENERAL_URL, component: , }, + { + path: APPLICATION_SETTINGS_CONFIGURE_URL, + component: , + }, { path: APPLICATION_SETTINGS_RESOURCES_URL, component: , diff --git a/libs/pages/application/src/lib/ui/import-environment-variable-modal/import-environment-variable-modal.tsx b/libs/pages/application/src/lib/ui/import-environment-variable-modal/import-environment-variable-modal.tsx index 1f205a229b..b142d8e06f 100644 --- a/libs/pages/application/src/lib/ui/import-environment-variable-modal/import-environment-variable-modal.tsx +++ b/libs/pages/application/src/lib/ui/import-environment-variable-modal/import-environment-variable-modal.tsx @@ -15,8 +15,8 @@ import { InputTextSmall, InputToggle, } from '@qovery/shared/ui' +import { computeAvailableScope } from '@qovery/shared/utils' import { validateKey, warningMessage } from '../../feature/import-environment-variable-modal-feature/utils/form-check' -import { computeAvailableScope } from '../../utils/compute-available-environment-variable-scope' export interface ImportEnvironmentVariableModalProps { onSubmit: () => void diff --git a/libs/pages/application/src/lib/ui/need-redeploy-flag/need-redeploy-flag.tsx b/libs/pages/application/src/lib/ui/need-redeploy-flag/need-redeploy-flag.tsx index 6e368486a6..e68ce6ac43 100644 --- a/libs/pages/application/src/lib/ui/need-redeploy-flag/need-redeploy-flag.tsx +++ b/libs/pages/application/src/lib/ui/need-redeploy-flag/need-redeploy-flag.tsx @@ -1,4 +1,5 @@ import { ServiceDeploymentStatusEnum } from 'qovery-typescript-axios' +import { isJob } from '@qovery/shared/enums' import { ApplicationEntity } from '@qovery/shared/interfaces' import { Banner, BannerStyle, IconAwesomeEnum } from '@qovery/shared/ui' @@ -21,7 +22,7 @@ export function NeedRedeployFlag(props: NeedRedeployFlagProps) { onClickButton={props.onClickCTA} >

- This application needs to be{' '} + This {isJob(props.application) ? 'job' : 'application'} needs to be{' '} {props.application.status?.service_deployment_status === ServiceDeploymentStatusEnum.OUT_OF_DATE ? 'redeployed' : 'deployed'}{' '} diff --git a/libs/pages/application/src/lib/ui/page-settings-advanced/page-settings-advanced.spec.tsx b/libs/pages/application/src/lib/ui/page-settings-advanced/page-settings-advanced.spec.tsx index 0d76d0b016..245d5cd0ed 100644 --- a/libs/pages/application/src/lib/ui/page-settings-advanced/page-settings-advanced.spec.tsx +++ b/libs/pages/application/src/lib/ui/page-settings-advanced/page-settings-advanced.spec.tsx @@ -1,4 +1,4 @@ -import { act, fireEvent, getByLabelText } from '@testing-library/react' +import { act, fireEvent } from '@testing-library/react' import { render } from '__tests__/utils/setup-jest' import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' import PageSettingsAdvanced, { PageSettingsAdvancedProps } from './page-settings-advanced' diff --git a/libs/pages/application/src/lib/ui/page-settings-configure-job/page-settings-configure-job.spec.tsx b/libs/pages/application/src/lib/ui/page-settings-configure-job/page-settings-configure-job.spec.tsx new file mode 100644 index 0000000000..baeaa48ed4 --- /dev/null +++ b/libs/pages/application/src/lib/ui/page-settings-configure-job/page-settings-configure-job.spec.tsx @@ -0,0 +1,29 @@ +import { render } from '__tests__/utils/setup-jest' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { cronjobFactoryMock } from '@qovery/domains/application' +import { JobConfigureData } from '@qovery/shared/interfaces' +import PageSettingsConfigureJob, { PageSettingsConfigureJobProps } from './page-settings-configure-job' + +const props: PageSettingsConfigureJobProps = { + application: cronjobFactoryMock(1)[0], + loading: false, + onSubmit: jest.fn(), +} + +describe('PageSettingsDangerZone', () => { + it('should render successfully', () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues: { + max_duration: 1, + nb_restarts: 1, + port: 3000, + schedule: '0 0 * * *', + cmd_arguments: '', + cmd: [''], + }, + }) + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/application/src/lib/ui/page-settings-configure-job/page-settings-configure-job.tsx b/libs/pages/application/src/lib/ui/page-settings-configure-job/page-settings-configure-job.tsx new file mode 100644 index 0000000000..78244aa42c --- /dev/null +++ b/libs/pages/application/src/lib/ui/page-settings-configure-job/page-settings-configure-job.tsx @@ -0,0 +1,61 @@ +import { useFormContext } from 'react-hook-form' +import { JobConfigureSettings } from '@qovery/shared/console-shared' +import { ServiceTypeEnum, isCronJob } from '@qovery/shared/enums' +import { ApplicationEntity } from '@qovery/shared/interfaces' +import { BlockContent, Button, ButtonSize, ButtonStyle, HelpSection } from '@qovery/shared/ui' + +export interface PageSettingsConfigureJobProps { + application?: ApplicationEntity + loading?: boolean + onSubmit: () => void +} + +export function PageSettingsConfigureJob(props: PageSettingsConfigureJobProps) { + const { loading, onSubmit } = props + const { formState } = useFormContext() + + return ( +

+
+
+ + + +
+ +
+
+
+ +
+ ) +} + +export default PageSettingsConfigureJob diff --git a/libs/pages/application/src/lib/ui/page-settings-general/page-settings-general.tsx b/libs/pages/application/src/lib/ui/page-settings-general/page-settings-general.tsx index 11b02ed186..2ce8a01ac4 100644 --- a/libs/pages/application/src/lib/ui/page-settings-general/page-settings-general.tsx +++ b/libs/pages/application/src/lib/ui/page-settings-general/page-settings-general.tsx @@ -1,12 +1,16 @@ import { BuildModeEnum, BuildPackLanguageEnum } from 'qovery-typescript-axios' import { FormEventHandler } from 'react' import { Controller, useFormContext } from 'react-hook-form' -import { GeneralContainerSettings } from '@qovery/shared/console-shared' -import { ServiceTypeEnum, isApplication, isContainer } from '@qovery/shared/enums' +import { + EditGitRepositorySettingsFeature, + EntrypointCmdInputs, + GeneralContainerSettings, + JobGeneralSettings, +} from '@qovery/shared/console-shared' +import { ServiceTypeEnum, isApplication, isContainer, isCronJob, isJob } from '@qovery/shared/enums' import { OrganizationEntity } from '@qovery/shared/interfaces' import { BlockContent, Button, ButtonSize, ButtonStyle, HelpSection, InputSelect, InputText } from '@qovery/shared/ui' import { upperCaseFirstLetter } from '@qovery/shared/utils' -import GitRepositorySettingsFeature from '../../feature/git-repository-settings-feature/git-repository-settings-feature' export interface PageSettingsGeneralProps { onSubmit: FormEventHandler @@ -52,14 +56,22 @@ export function PageSettingsGeneral(props: PageSettingsGeneralProps) { )} /> + {isJob(type) && ( + + )} {isContainer(type) && ( + )} {isApplication(type) && ( <> - + ({ describe('PageSettingsResources', () => { window.ResizeObserver = ResizeObserver - let defaultValues: ResourcesData + let defaultValues: ApplicationResourcesData beforeEach(() => { defaultValues = { diff --git a/libs/pages/login/src/lib/feature/page-logout-feature/page-logout-feature.spec.tsx b/libs/pages/login/src/lib/feature/page-logout-feature/page-logout-feature.spec.tsx index b6af3473c3..6fafb3a872 100644 --- a/libs/pages/login/src/lib/feature/page-logout-feature/page-logout-feature.spec.tsx +++ b/libs/pages/login/src/lib/feature/page-logout-feature/page-logout-feature.spec.tsx @@ -1,9 +1,23 @@ -import PageLogoutFeature from './page-logout-feature' import { render } from '__tests__/utils/setup-jest' +import posthog from 'posthog-js' +import React from 'react' +import PageLogoutFeature from './page-logout-feature' + +const PostHogWrapper = ({ children }: { children: React.ReactElement }) => { + posthog.init('__test__posthog__token', { + api_host: '__test__environment__posthog__apihost', + }) + + return children +} describe('PageLogoutFeature', () => { it('should render successfully', () => { - const { baseElement } = render() + const { baseElement } = render( + + + + ) expect(baseElement).toBeTruthy() }) }) diff --git a/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-feature.tsx b/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-feature.tsx index c6add97036..ac1e064c39 100644 --- a/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-feature.tsx +++ b/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-feature.tsx @@ -1,20 +1,20 @@ import { createContext, useContext, useState } from 'react' import { Navigate, Route, Routes, useNavigate, useParams } from 'react-router-dom' +import { ApplicationGeneralData, ApplicationResourcesData, FlowPortData } from '@qovery/shared/interfaces' import { SERVICES_APPLICATION_CREATION_URL, SERVICES_CREATION_GENERAL_URL, SERVICES_URL } from '@qovery/shared/router' import { FunnelFlow } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/utils' import { ROUTER_SERVICE_CREATION } from '../../router/router' -import { GeneralData, PortData, ResourcesData } from './application-creation-flow.interface' export interface ApplicationContainerCreateContextInterface { currentStep: number setCurrentStep: (step: number) => void - generalData: GeneralData | undefined - setGeneralData: (data: GeneralData) => void - resourcesData: ResourcesData | undefined - setResourcesData: (data: ResourcesData) => void - portData: PortData | undefined - setPortData: (data: PortData) => void + generalData: ApplicationGeneralData | undefined + setGeneralData: (data: ApplicationGeneralData) => void + resourcesData: ApplicationResourcesData | undefined + setResourcesData: (data: ApplicationResourcesData) => void + portData: FlowPortData | undefined + setPortData: (data: FlowPortData) => void } export const ApplicationContainerCreateContext = createContext( @@ -41,14 +41,14 @@ export function PageApplicationCreateFeature() { // values and setters for context initialization const [currentStep, setCurrentStep] = useState(1) - const [generalData, setGeneralData] = useState() - const [resourcesData, setResourcesData] = useState({ + const [generalData, setGeneralData] = useState() + const [resourcesData, setResourcesData] = useState({ memory: 512, cpu: [0.5], instances: [1, 2], }) - const [portData, setPortData] = useState({ + const [portData, setPortData] = useState({ ports: [], }) diff --git a/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-general-feature/page-application-create-general-feature.tsx b/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-general-feature/page-application-create-general-feature.tsx index 69ec836069..8826dd8aa7 100644 --- a/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-general-feature/page-application-create-general-feature.tsx +++ b/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-general-feature/page-application-create-general-feature.tsx @@ -4,14 +4,13 @@ import { useDispatch, useSelector } from 'react-redux' import { useNavigate, useParams } from 'react-router-dom' import { fetchOrganizationContainerRegistries, selectOrganizationById } from '@qovery/domains/organization' import { isContainer } from '@qovery/shared/enums' -import { OrganizationEntity } from '@qovery/shared/interfaces' +import { ApplicationGeneralData, OrganizationEntity } from '@qovery/shared/interfaces' import { SERVICES_APPLICATION_CREATION_URL, SERVICES_CREATION_RESOURCES_URL, SERVICES_URL } from '@qovery/shared/router' import { toastError } from '@qovery/shared/toast' import { FunnelFlowBody, FunnelFlowHelpCard } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/utils' import { AppDispatch, RootState } from '@qovery/store' import PageApplicationCreateGeneral from '../../../ui/page-application-create/page-application-create-general/page-application-create-general' -import { GeneralData } from '../application-creation-flow.interface' import { useApplicationContainerCreateContext } from '../page-application-create-feature' export function PageApplicationCreateGeneralFeature() { @@ -48,7 +47,7 @@ export function PageApplicationCreateGeneralFeature() { setCurrentStep(1) }, [setCurrentStep]) - const methods = useForm({ + const methods = useForm({ defaultValues: generalData, mode: 'onChange', }) diff --git a/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-port-feature/page-application-create-port-feature.tsx b/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-port-feature/page-application-create-port-feature.tsx index bcde2196e0..d96b7ed03a 100644 --- a/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-port-feature/page-application-create-port-feature.tsx +++ b/libs/pages/services/src/lib/feature/page-application-create-feature/page-application-create-port-feature/page-application-create-port-feature.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react' import { FormProvider, useForm } from 'react-hook-form' import { useNavigate, useParams } from 'react-router-dom' +import { FlowCreatePort } from '@qovery/shared/console-shared' +import { FlowPortData } from '@qovery/shared/interfaces' import { SERVICES_APPLICATION_CREATION_URL, SERVICES_CREATION_GENERAL_URL, @@ -10,8 +12,6 @@ import { } from '@qovery/shared/router' import { FunnelFlowBody, FunnelFlowHelpCard } from '@qovery/shared/ui' import { useDocumentTitle } from '@qovery/shared/utils' -import PageApplicationCreatePort from '../../../ui/page-application-create/page-application-create-port/page-application-create-port' -import { PortData } from '../application-creation-flow.interface' import { useApplicationContainerCreateContext } from '../page-application-create-feature' export function PageApplicationCreatePortFeature() { @@ -56,7 +56,7 @@ export function PageApplicationCreatePortFeature() { setCurrentStep(3) }, [setCurrentStep]) - const methods = useForm({ + const methods = useForm({ defaultValues: portData, mode: 'onChange', }) @@ -93,7 +93,7 @@ export function PageApplicationCreatePortFeature() { return ( - ({ + const methods = useForm({ defaultValues: resourcesData, mode: 'onChange', }) diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/page-job-create-feature.spec.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/page-job-create-feature.spec.tsx new file mode 100644 index 0000000000..e8609d5161 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/page-job-create-feature.spec.tsx @@ -0,0 +1,22 @@ +import { render } from '__tests__/utils/setup-jest' +import { Route, Routes } from 'react-router' +import PageJobCreateFeature from './page-job-create-feature' + +jest.mock('react-router-dom', () => ({ + ...(jest.requireActual('react-router') as any), + useParams: () => ({ organizationId: '1', projectId: '2', environmentId: '3' }), +})) + +describe('PageJobCreateFeature', () => { + it('should render successfully', () => { + const { baseElement } = render( + + } + /> + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/page-job-create-feature.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/page-job-create-feature.tsx new file mode 100644 index 0000000000..f969ffc143 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/page-job-create-feature.tsx @@ -0,0 +1,127 @@ +import { createContext, useContext, useEffect, useState } from 'react' +import { Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom' +import { JobType, ServiceTypeEnum } from '@qovery/shared/enums' +import { FlowVariableData, JobConfigureData, JobGeneralData, JobResourcesData } from '@qovery/shared/interfaces' +import { + SERVICES_CRONJOB_CREATION_URL, + SERVICES_JOB_CREATION_GENERAL_URL, + SERVICES_LIFECYCLE_CREATION_URL, + SERVICES_URL, +} from '@qovery/shared/router' +import { FunnelFlow } from '@qovery/shared/ui' +import { useDocumentTitle } from '@qovery/shared/utils' +import { ROUTER_SERVICE_JOB_CREATION } from '../../router/router' + +export interface JobContainerCreateContextInterface { + currentStep: number + setCurrentStep: (step: number) => void + generalData: JobGeneralData | undefined + setGeneralData: (data: JobGeneralData) => void + + configureData: JobConfigureData | undefined + setConfigureData: (data: JobConfigureData) => void + + resourcesData: JobResourcesData | undefined + setResourcesData: (data: JobResourcesData) => void + + variableData: FlowVariableData | undefined + setVariableData: (data: FlowVariableData) => void + + jobType: JobType + jobURL: string | undefined +} + +export const JobContainerCreateContext = createContext(undefined) + +// this is to avoid to set initial value twice https://stackoverflow.com/questions/49949099/react-createcontext-point-of-defaultvalue +export const useJobContainerCreateContext = () => { + const applicationContainerCreateContext = useContext(JobContainerCreateContext) + if (!applicationContainerCreateContext) + throw new Error('useJobContainerCreateContext must be used within a JobContainerCreateContext') + return applicationContainerCreateContext +} + +export const steps: { title: string }[] = [ + { title: 'Create new job' }, + { title: 'Job Configuration' }, + { title: 'Set resources' }, + { title: 'Set port' }, + { title: 'Set variable environments' }, + { title: 'Ready to install' }, +] + +export function PageJobCreateFeature() { + const { organizationId = '', projectId = '', environmentId = '' } = useParams() + const location = useLocation() + + // values and setters for context initialization + const [currentStep, setCurrentStep] = useState(1) + const [generalData, setGeneralData] = useState() + const [jobType, setJobType] = useState(ServiceTypeEnum.CRON_JOB) + const [jobURL, setJobURL] = useState() + const [configureData, setConfigureData] = useState() + const [resourcesData, setResourcesData] = useState({ + memory: 512, + cpu: [0.5], + }) + + const [variableData, setVariableData] = useState({ + variables: [], + }) + + const navigate = useNavigate() + + useDocumentTitle('Creation - Job') + + useEffect(() => { + if (location.pathname.indexOf('cron') !== -1) { + setJobURL(SERVICES_CRONJOB_CREATION_URL) + setJobType(ServiceTypeEnum.CRON_JOB) + } else { + setJobType(ServiceTypeEnum.LIFECYCLE_JOB) + setJobURL(SERVICES_LIFECYCLE_CREATION_URL) + } + }, [setJobURL, setJobType, location.pathname]) + + const pathCreate = `${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + + return ( + + { + navigate(SERVICES_URL(organizationId, projectId, environmentId)) + }} + totalSteps={5} + currentStep={currentStep} + currentTitle={steps[currentStep - 1].title} + portal + > + + {ROUTER_SERVICE_JOB_CREATION.map((route) => ( + + ))} + {jobURL && ( + } /> + )} + + + + ) +} + +export default PageJobCreateFeature diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-configure-feature/step-configure-feature.spec.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-configure-feature/step-configure-feature.spec.tsx new file mode 100644 index 0000000000..90fb2dc890 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-configure-feature/step-configure-feature.spec.tsx @@ -0,0 +1,32 @@ +import { render } from '__tests__/utils/setup-jest' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobContainerCreateContext } from '../page-job-create-feature' +import StepConfigureFeature from './step-configure-feature' + +describe('GeneralFeature', () => { + it('should render successfully', () => { + const { baseElement } = render( + + + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-configure-feature/step-configure-feature.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-configure-feature/step-configure-feature.tsx new file mode 100644 index 0000000000..fea1736ed2 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-configure-feature/step-configure-feature.tsx @@ -0,0 +1,122 @@ +import { useEffect } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { useNavigate, useParams } from 'react-router-dom' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobConfigureData } from '@qovery/shared/interfaces' +import { + SERVICES_JOB_CREATION_GENERAL_URL, + SERVICES_JOB_CREATION_RESOURCES_URL, + SERVICES_URL, +} from '@qovery/shared/router' +import { toastError } from '@qovery/shared/toast' +import { FunnelFlowBody, FunnelFlowHelpCard } from '@qovery/shared/ui' +import { useDocumentTitle } from '@qovery/shared/utils' +import StepConfigure from '../../../ui/page-job-create/step-configure/step-configure' +import { useJobContainerCreateContext } from '../page-job-create-feature' + +export function StepConfigureFeature() { + useDocumentTitle('Configure - Create Job') + const { configureData, setConfigureData, setCurrentStep, generalData, jobURL, jobType } = + useJobContainerCreateContext() + const { organizationId = '', projectId = '', environmentId = '' } = useParams() + const navigate = useNavigate() + + const funnelCardHelp = ( + + ) + + useEffect(() => { + !generalData?.name && + jobURL && + navigate(`${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + SERVICES_JOB_CREATION_GENERAL_URL) + }, [generalData, navigate, environmentId, organizationId, projectId, jobURL]) + + useEffect(() => { + setCurrentStep(2) + + if (configureData?.nb_restarts === undefined) { + methods.setValue('nb_restarts', 0) + } + + if (configureData?.max_duration === undefined) { + methods.setValue('max_duration', 300) + } + }, [setCurrentStep]) + + const methods = useForm({ + defaultValues: configureData, + mode: 'onChange', + }) + + const onSubmit = methods.handleSubmit((data) => { + const cloneData: JobConfigureData = { + ...data, + } + + if (jobType === ServiceTypeEnum.CRON_JOB) { + if (data.cmd_arguments) { + try { + cloneData.cmd = eval(data.cmd_arguments) + } catch (e: any) { + toastError(e, 'Invalid CMD array') + return + } + } + } + + if (jobType === ServiceTypeEnum.LIFECYCLE_JOB) { + if (cloneData.on_start?.enabled && cloneData.on_start?.arguments_string) { + try { + cloneData.on_start.arguments = eval(cloneData.on_start.arguments_string) + } catch (e: any) { + toastError(e, 'Invalid CMD array') + return + } + } + + if (cloneData.on_stop?.enabled && cloneData.on_stop?.arguments_string) { + try { + cloneData.on_stop.arguments = eval(cloneData.on_stop.arguments_string) + } catch (e: any) { + toastError(e, 'Invalid CMD array') + return + } + } + + if (cloneData.on_delete?.enabled && cloneData.on_delete?.arguments_string) { + try { + cloneData.on_delete.arguments = eval(cloneData.on_delete.arguments_string) + } catch (e: any) { + toastError(e, 'Invalid CMD array') + return + } + } + } + + setConfigureData(cloneData) + const pathCreate = `${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + navigate(pathCreate + SERVICES_JOB_CREATION_RESOURCES_URL) + }) + + const onBack = () => { + const pathCreate = `${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + navigate(pathCreate + SERVICES_JOB_CREATION_GENERAL_URL) + } + + return ( + + + + + + ) +} + +export default StepConfigureFeature diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-general-feature/step-general-feature.spec.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-general-feature/step-general-feature.spec.tsx new file mode 100644 index 0000000000..c36b901731 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-general-feature/step-general-feature.spec.tsx @@ -0,0 +1,32 @@ +import { render } from '__tests__/utils/setup-jest' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobContainerCreateContext } from '../page-job-create-feature' +import StepGeneralFeature from './step-general-feature' + +describe('GeneralFeature', () => { + it('should render successfully', () => { + const { baseElement } = render( + + + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-general-feature/step-general-feature.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-general-feature/step-general-feature.tsx new file mode 100644 index 0000000000..5f25e09148 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-general-feature/step-general-feature.tsx @@ -0,0 +1,78 @@ +import { BuildModeEnum } from 'qovery-typescript-axios' +import { useEffect } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate, useParams } from 'react-router-dom' +import { fetchOrganizationContainerRegistries, selectOrganizationById } from '@qovery/domains/organization' +import { ServiceTypeEnum, isContainer } from '@qovery/shared/enums' +import { JobGeneralData, OrganizationEntity } from '@qovery/shared/interfaces' +import { SERVICES_JOB_CREATION_CONFIGURE_URL, SERVICES_URL } from '@qovery/shared/router' +import { FunnelFlowBody, FunnelFlowHelpCard } from '@qovery/shared/ui' +import { useDocumentTitle } from '@qovery/shared/utils' +import { AppDispatch, RootState } from '@qovery/store' +import StepGeneral from '../../../ui/page-job-create/step-general/step-general' +import { useJobContainerCreateContext } from '../page-job-create-feature' + +export function StepGeneralFeature() { + useDocumentTitle('General - Create Job') + const { setGeneralData, generalData, setCurrentStep, jobURL, jobType } = useJobContainerCreateContext() + const { organizationId = '', projectId = '', environmentId = '' } = useParams() + const navigate = useNavigate() + + const organization = useSelector((state) => + selectOrganizationById(state, organizationId) + ) + + const funnelCardHelp = ( + + ) + + useEffect(() => { + setCurrentStep(1) + }, [setCurrentStep]) + + const methods = useForm({ + defaultValues: generalData, + mode: 'onChange', + }) + + const dispatch = useDispatch() + const watchServiceType = methods.watch('serviceType') + + useEffect(() => { + methods.setValue('build_mode', BuildModeEnum.DOCKER) + }, [methods]) + + useEffect(() => { + if (isContainer(watchServiceType)) { + dispatch(fetchOrganizationContainerRegistries({ organizationId })) + } + }, [watchServiceType, dispatch, organizationId]) + + const onSubmit = methods.handleSubmit((data) => { + const cloneData = { + ...data, + } + + setGeneralData(cloneData) + const pathCreate = `${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + navigate(pathCreate + SERVICES_JOB_CREATION_CONFIGURE_URL) + }) + + return ( + + + + + + ) +} + +export default StepGeneralFeature diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-resources-feature/step-resources-feature.spec.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-resources-feature/step-resources-feature.spec.tsx new file mode 100644 index 0000000000..5b4ab3ab74 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-resources-feature/step-resources-feature.spec.tsx @@ -0,0 +1,32 @@ +import { render } from '__tests__/utils/setup-jest' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobContainerCreateContext } from '../page-job-create-feature' +import StepResourcesFeature from './step-resources-feature' + +describe('ResourcesFeature', () => { + it('should render successfully', () => { + const { baseElement } = render( + + + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-resources-feature/step-resources-feature.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-resources-feature/step-resources-feature.tsx new file mode 100644 index 0000000000..21c5f2137e --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-resources-feature/step-resources-feature.tsx @@ -0,0 +1,82 @@ +import { useEffect } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { useNavigate, useParams } from 'react-router-dom' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { ApplicationResourcesData } from '@qovery/shared/interfaces' +import { + SERVICES_JOB_CREATION_CONFIGURE_URL, + SERVICES_JOB_CREATION_GENERAL_URL, + SERVICES_JOB_CREATION_VARIABLE_URL, + SERVICES_URL, +} from '@qovery/shared/router' +import { FunnelFlowBody, FunnelFlowHelpCard } from '@qovery/shared/ui' +import { useDocumentTitle } from '@qovery/shared/utils' +import StepResources from '../../../ui/page-job-create/step-resources/step-resources' +import { useJobContainerCreateContext } from '../page-job-create-feature' + +export function StepResourcesFeature() { + useDocumentTitle('Resources - Create Job') + const { setCurrentStep, resourcesData, setResourcesData, generalData, jobURL, jobType } = + useJobContainerCreateContext() + const { organizationId = '', projectId = '', environmentId = '' } = useParams() + const navigate = useNavigate() + + useEffect(() => { + !generalData?.name && + jobURL && + navigate(`${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + SERVICES_JOB_CREATION_GENERAL_URL) + }, [generalData, navigate, environmentId, organizationId, jobURL, projectId]) + + const funnelCardHelp = ( + + ) + + useEffect(() => { + setCurrentStep(3) + }, [setCurrentStep]) + + const methods = useForm({ + defaultValues: resourcesData, + mode: 'onChange', + }) + + const onSubmit = methods.handleSubmit((data) => { + setResourcesData(data) + const pathCreate = `${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + navigate(pathCreate + SERVICES_JOB_CREATION_VARIABLE_URL) + }) + + const onBack = () => { + const pathCreate = `${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + navigate(pathCreate + SERVICES_JOB_CREATION_CONFIGURE_URL) + } + + return ( + + + + + + ) +} + +export default StepResourcesFeature diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-summary-feature/step-summary-feature.spec.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-summary-feature/step-summary-feature.spec.tsx new file mode 100644 index 0000000000..27a50968c0 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-summary-feature/step-summary-feature.spec.tsx @@ -0,0 +1,32 @@ +import { render } from '__tests__/utils/setup-jest' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobContainerCreateContext } from '../page-job-create-feature' +import StepSummaryFeature from './step-summary-feature' + +describe('PostFeature', () => { + it('should render successfully', () => { + const { baseElement } = render( + + + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-summary-feature/step-summary-feature.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-summary-feature/step-summary-feature.tsx new file mode 100644 index 0000000000..aef9cf00b6 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-summary-feature/step-summary-feature.tsx @@ -0,0 +1,267 @@ +import { APIVariableScopeEnum, JobRequest, VariableImportRequest } from 'qovery-typescript-axios' +import { useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { useNavigate, useParams } from 'react-router-dom' +import { createApplication, postApplicationActionsDeploy } from '@qovery/domains/application' +import { importEnvironmentVariables } from '@qovery/domains/environment-variable' +import { selectAllRepository, selectOrganizationById } from '@qovery/domains/organization' +import { JobType, ServiceTypeEnum } from '@qovery/shared/enums' +import { + FlowVariableData, + JobConfigureData, + JobGeneralData, + JobResourcesData, + OrganizationEntity, + RepositoryEntity, +} from '@qovery/shared/interfaces' +import { + SERVICES_JOB_CREATION_CONFIGURE_URL, + SERVICES_JOB_CREATION_GENERAL_URL, + SERVICES_JOB_CREATION_RESOURCES_URL, + SERVICES_JOB_CREATION_VARIABLE_URL, + SERVICES_URL, +} from '@qovery/shared/router' +import { FunnelFlowBody } from '@qovery/shared/ui' +import { buildGitRepoUrl, convertCpuToVCpu, useDocumentTitle } from '@qovery/shared/utils' +import { AppDispatch, RootState } from '@qovery/store' +import StepSummary from '../../../ui/page-job-create/step-summary/step-summary' +import { useJobContainerCreateContext } from '../page-job-create-feature' + +function prepareJobRequest( + generalData: JobGeneralData, + configureData: JobConfigureData, + resourcesData: JobResourcesData, + selectedRepository: RepositoryEntity | undefined, + jobType: JobType +): JobRequest { + const memory = Number(resourcesData['memory']) + const cpu = convertCpuToVCpu(resourcesData['cpu'][0], true) + + const jobRequest: JobRequest = { + name: generalData.name, + port: Number(configureData.port), + description: generalData.description || '', + cpu: cpu, + memory: memory, + max_nb_restart: Number(configureData.nb_restarts) || 0, + max_duration_seconds: Number(configureData.max_duration) || 0, + auto_preview: true, + } + + if (jobType === ServiceTypeEnum.CRON_JOB) { + jobRequest.schedule = { + cronjob: { + entrypoint: configureData.image_entry_point, + scheduled_at: configureData.schedule || '', + arguments: configureData.cmd || [''], + }, + } + } else { + jobRequest.schedule = { + on_start: { + entrypoint: configureData.on_start?.entrypoint, + arguments: configureData.on_start?.arguments, + }, + on_stop: { + entrypoint: configureData.on_stop?.entrypoint, + arguments: configureData.on_stop?.arguments, + }, + on_delete: { + entrypoint: configureData.on_delete?.entrypoint, + arguments: configureData.on_delete?.arguments, + }, + } + + if (!configureData.on_start?.enabled) { + delete jobRequest.schedule.on_start + } + + if (!configureData.on_stop?.enabled) { + delete jobRequest.schedule.on_stop + } + + if (!configureData.on_delete?.enabled) { + delete jobRequest.schedule.on_delete + } + } + + if (generalData.serviceType === ServiceTypeEnum.CONTAINER) { + jobRequest.source = { + image: { + tag: generalData.image_tag, + image_name: generalData.image_name, + registry_id: generalData.registry, + }, + } + } else { + jobRequest.source = { + docker: { + dockerfile_path: generalData.dockerfile_path, + git_repository: { + url: buildGitRepoUrl(generalData.provider || '', selectedRepository?.url || '') || '', + root_path: generalData.root_path, + branch: generalData.branch, + }, + }, + } + } + + return jobRequest +} + +function prepareVariableRequest(variablesData: FlowVariableData): VariableImportRequest | null { + if (variablesData.variables && variablesData.variables.length === 0) { + return null + } + + return { + overwrite: true, + vars: variablesData.variables.map((variable) => ({ + name: variable.variable || '', + scope: variable.scope || APIVariableScopeEnum.PROJECT, + value: variable.value || '', + is_secret: variable.isSecret, + })), + } +} + +export function StepSummaryFeature() { + useDocumentTitle('Summary - Create Application') + const { generalData, resourcesData, configureData, setCurrentStep, jobURL, variableData, jobType } = + useJobContainerCreateContext() + const navigate = useNavigate() + const { organizationId = '', projectId = '', environmentId = '' } = useParams() + const pathCreate = `${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + const [loadingCreate, setLoadingCreate] = useState(false) + const [loadingCreateAndDeploy, setLoadingCreateAndDeploy] = useState(false) + const organization = useSelector((state) => + selectOrganizationById(state, organizationId) + ) + const repositories = useSelector(selectAllRepository) + const selectedRepository = repositories.find((repository) => repository.name === generalData?.repository) + + const gotoGlobalInformations = () => { + navigate(pathCreate + SERVICES_JOB_CREATION_GENERAL_URL) + } + + const gotoResources = () => { + navigate(pathCreate + SERVICES_JOB_CREATION_RESOURCES_URL) + } + + const gotoConfigureJob = () => { + navigate(pathCreate + SERVICES_JOB_CREATION_CONFIGURE_URL) + } + + const gotoVariable = () => { + navigate(pathCreate + SERVICES_JOB_CREATION_VARIABLE_URL) + } + + useEffect(() => { + !generalData?.name && + jobURL && + navigate(`${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + SERVICES_JOB_CREATION_GENERAL_URL) + }, [generalData, navigate, environmentId, organizationId, projectId, jobURL, gotoGlobalInformations]) + + const dispatch = useDispatch() + + const onSubmit = (withDeploy: boolean) => { + if (generalData && resourcesData && variableData && configureData) { + toggleLoading(true, withDeploy) + + const jobRequest: JobRequest = prepareJobRequest( + generalData, + configureData, + resourcesData, + selectedRepository, + jobType + ) + const variableRequest = prepareVariableRequest(variableData) + + dispatch( + createApplication({ + environmentId: environmentId, + data: jobRequest, + serviceType: ServiceTypeEnum.JOB, + }) + ) + .unwrap() + .then((app) => { + if (variableRequest) { + dispatch( + importEnvironmentVariables({ + vars: variableRequest.vars, + serviceType: ServiceTypeEnum.JOB, + overwriteEnabled: variableRequest.overwrite, + applicationId: app.id, + }) + ) + .unwrap() + .then(() => { + toggleLoading(false, withDeploy) + if (withDeploy) { + dispatch( + postApplicationActionsDeploy({ + environmentId, + applicationId: app.id, + serviceType: ServiceTypeEnum.JOB, + }) + ) + } + navigate(SERVICES_URL(organizationId, projectId, environmentId)) + }) + } else { + if (withDeploy) { + dispatch( + postApplicationActionsDeploy({ + environmentId, + applicationId: app.id, + serviceType: ServiceTypeEnum.JOB, + }) + ) + } + navigate(SERVICES_URL(organizationId, projectId, environmentId)) + } + }) + .catch((e) => console.error(e)) + .finally(() => { + toggleLoading(false, withDeploy) + }) + } + } + + const toggleLoading = (value: boolean, withDeploy = false) => { + if (withDeploy) setLoadingCreateAndDeploy(value) + else setLoadingCreate(value) + } + + useEffect(() => { + setCurrentStep(5) + }, [setCurrentStep]) + + return ( + + {generalData && resourcesData && variableData && configureData && ( + registry.id === generalData.registry)?.name + } + jobType={jobType} + gotoConfigureJob={gotoConfigureJob} + /> + )} + + ) +} + +export default StepSummaryFeature diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-variable-feature/step-variable-feature.spec.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-variable-feature/step-variable-feature.spec.tsx new file mode 100644 index 0000000000..ea7a434fa7 --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-variable-feature/step-variable-feature.spec.tsx @@ -0,0 +1,32 @@ +import { render } from '__tests__/utils/setup-jest' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobContainerCreateContext } from '../page-job-create-feature' +import StepVariableFeature from './step-variable-feature' + +describe('StepVariableFeature', () => { + it('should render successfully', () => { + const { baseElement } = render( + + + + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/feature/page-job-create-feature/step-variable-feature/step-variable-feature.tsx b/libs/pages/services/src/lib/feature/page-job-create-feature/step-variable-feature/step-variable-feature.tsx new file mode 100644 index 0000000000..2202ac0a3b --- /dev/null +++ b/libs/pages/services/src/lib/feature/page-job-create-feature/step-variable-feature/step-variable-feature.tsx @@ -0,0 +1,96 @@ +import { APIVariableScopeEnum } from 'qovery-typescript-axios' +import { useEffect, useState } from 'react' +import { FormProvider, useForm } from 'react-hook-form' +import { useNavigate, useParams } from 'react-router-dom' +import { FlowCreateVariable } from '@qovery/shared/console-shared' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { FlowVariableData, VariableData } from '@qovery/shared/interfaces' +import { + SERVICES_JOB_CREATION_GENERAL_URL, + SERVICES_JOB_CREATION_POST_URL, + SERVICES_JOB_CREATION_RESOURCES_URL, + SERVICES_URL, +} from '@qovery/shared/router' +import { FunnelFlowBody, FunnelFlowHelpCard } from '@qovery/shared/ui' +import { computeAvailableScope, useDocumentTitle } from '@qovery/shared/utils' +import { useJobContainerCreateContext } from '../page-job-create-feature' + +export function StepVariableFeature() { + useDocumentTitle('Environment Variable - Create Job') + const { setCurrentStep, generalData, setVariableData, variableData, jobURL, jobType } = useJobContainerCreateContext() + const { organizationId = '', projectId = '', environmentId = '' } = useParams() + const navigate = useNavigate() + const pathCreate = `${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + const [availableScopes] = useState(computeAvailableScope()) + + useEffect(() => { + !generalData?.name && + jobURL && + navigate(`${SERVICES_URL(organizationId, projectId, environmentId)}${jobURL}` + SERVICES_JOB_CREATION_GENERAL_URL) + }, [generalData, navigate, environmentId, organizationId, projectId, jobURL]) + + const funnelCardHelp = ( + + ) + const methods = useForm({ + defaultValues: variableData, + mode: 'onChange', + }) + + const onSubmit = methods.handleSubmit((data) => { + setVariableData(data) + navigate(pathCreate + SERVICES_JOB_CREATION_POST_URL) + }) + + const onBack = () => { + navigate(pathCreate + SERVICES_JOB_CREATION_RESOURCES_URL) + } + + const [variables, setVariables] = useState(methods.getValues().variables) + + const onAddPort = () => { + const newVariableRow: VariableData = { variable: undefined, isSecret: false, value: undefined, scope: undefined } + if (variables.length) { + setVariables([...variables, newVariableRow]) + methods.setValue(`variables.${variables.length}`, newVariableRow) + } else { + setVariables([newVariableRow]) + methods.setValue(`variables.0`, newVariableRow) + } + } + + const removePort = (index: number) => { + const newVariables = methods.getValues().variables + newVariables.splice(index, 1) + setVariables(newVariables) + methods.reset({ variables: newVariables }) + } + + useEffect(() => { + setCurrentStep(4) + }, [setCurrentStep, variableData]) + + return ( + + + + + + ) +} + +export default StepVariableFeature diff --git a/libs/pages/services/src/lib/router/router.tsx b/libs/pages/services/src/lib/router/router.tsx index 803cacecc7..556b4a3806 100644 --- a/libs/pages/services/src/lib/router/router.tsx +++ b/libs/pages/services/src/lib/router/router.tsx @@ -6,12 +6,19 @@ import { SERVICES_CREATION_PORTS_URL, SERVICES_CREATION_POST_URL, SERVICES_CREATION_RESOURCES_URL, + SERVICES_CRONJOB_CREATION_URL, SERVICES_DATABASE_CREATION_GENERAL_URL, SERVICES_DATABASE_CREATION_POST_URL, SERVICES_DATABASE_CREATION_RESOURCES_URL, SERVICES_DATABASE_CREATION_URL, SERVICES_DEPLOYMENTS_URL, SERVICES_GENERAL_URL, + SERVICES_JOB_CREATION_CONFIGURE_URL, + SERVICES_JOB_CREATION_GENERAL_URL, + SERVICES_JOB_CREATION_POST_URL, + SERVICES_JOB_CREATION_RESOURCES_URL, + SERVICES_JOB_CREATION_VARIABLE_URL, + SERVICES_LIFECYCLE_CREATION_URL, SERVICES_SETTINGS_ADVANCED_SETTINGS_URL, SERVICES_SETTINGS_DANGER_ZONE_URL, SERVICES_SETTINGS_DEPLOYMENT_URL, @@ -29,6 +36,12 @@ import PageDatabaseCreatePostFeature from '../feature/page-database-create-featu import PageDatabaseCreateResourcesFeature from '../feature/page-database-create-feature/page-database-create-resources-feature/page-database-create-resources-feature' import PageDeploymentsFeature from '../feature/page-deployments-feature/page-deployments-feature' import PageGeneralFeature from '../feature/page-general-feature/page-general-feature' +import { PageJobCreateFeature } from '../feature/page-job-create-feature/page-job-create-feature' +import StepConfigureFeature from '../feature/page-job-create-feature/step-configure-feature/step-configure-feature' +import { StepGeneralFeature } from '../feature/page-job-create-feature/step-general-feature/step-general-feature' +import { StepResourcesFeature } from '../feature/page-job-create-feature/step-resources-feature/step-resources-feature' +import { StepSummaryFeature } from '../feature/page-job-create-feature/step-summary-feature/step-summary-feature' +import { StepVariableFeature } from '../feature/page-job-create-feature/step-variable-feature/step-variable-feature' import PageSettingsDangerZoneFeature from '../feature/page-settings-danger-zone-feature/page-settings-danger-zone-feature' import PageSettingsDeploymentFeature from '../feature/page-settings-deployment-feature/page-settings-deployment-feature' import { PageSettingsFeature } from '../feature/page-settings-feature/page-settings-feature' @@ -53,6 +66,14 @@ export const ROUTER_SERVICES: Route[] = [ path: `${SERVICES_DATABASE_CREATION_URL}/*`, component: , }, + { + path: `${SERVICES_CRONJOB_CREATION_URL}/*`, + component: , + }, + { + path: `${SERVICES_LIFECYCLE_CREATION_URL}/*`, + component: , + }, { path: `${SERVICES_APPLICATION_CREATION_URL}/*`, component: , @@ -115,3 +136,27 @@ export const ROUTER_SERVICE_DATABASE_CREATION: Route[] = [ component: , }, ] + +export const ROUTER_SERVICE_JOB_CREATION: Route[] = [ + { + path: SERVICES_JOB_CREATION_GENERAL_URL, + component: , + }, + { + path: SERVICES_JOB_CREATION_CONFIGURE_URL, + component: , + }, + { + path: SERVICES_JOB_CREATION_RESOURCES_URL, + component: , + }, + + { + path: SERVICES_JOB_CREATION_VARIABLE_URL, + component: , + }, + { + path: SERVICES_JOB_CREATION_POST_URL, + component: , + }, +] diff --git a/libs/pages/services/src/lib/ui/container/container.tsx b/libs/pages/services/src/lib/ui/container/container.tsx index 6ffc349817..5cc05bd909 100644 --- a/libs/pages/services/src/lib/ui/container/container.tsx +++ b/libs/pages/services/src/lib/ui/container/container.tsx @@ -8,9 +8,11 @@ import { IconEnum, RunningStatus } from '@qovery/shared/enums' import { ApplicationEntity, DatabaseEntity, EnvironmentEntity } from '@qovery/shared/interfaces' import { SERVICES_APPLICATION_CREATION_URL, + SERVICES_CRONJOB_CREATION_URL, SERVICES_DATABASE_CREATION_URL, SERVICES_DEPLOYMENTS_URL, SERVICES_GENERAL_URL, + SERVICES_LIFECYCLE_CREATION_URL, SERVICES_SETTINGS_URL, SERVICES_URL, } from '@qovery/shared/router' @@ -149,6 +151,24 @@ export function Container(props: ContainerProps) { navigate(`${SERVICES_URL(organizationId, projectId, environmentId)}${SERVICES_DATABASE_CREATION_URL}`) }, }, + { + name: 'Create lifecycle job', + contentLeft: ( + + ), + onClick: () => { + navigate(`${SERVICES_URL(organizationId, projectId, environmentId)}${SERVICES_LIFECYCLE_CREATION_URL}`) + }, + }, + { + name: 'Create cronjob', + contentLeft: ( + + ), + onClick: () => { + navigate(`${SERVICES_URL(organizationId, projectId, environmentId)}${SERVICES_CRONJOB_CREATION_URL}`) + }, + }, ], }, ] diff --git a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/page-application-create-general.tsx b/libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/page-application-create-general.tsx index 2bfcbdb517..f2da85bc43 100644 --- a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/page-application-create-general.tsx +++ b/libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/page-application-create-general.tsx @@ -1,13 +1,15 @@ import { FormEventHandler } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { useNavigate, useParams } from 'react-router-dom' -import { GeneralContainerSettings } from '@qovery/shared/console-shared' +import { + CreateGeneralGitApplication, + EntrypointCmdInputs, + GeneralContainerSettings, +} from '@qovery/shared/console-shared' import { IconEnum, ServiceTypeEnum, isApplication, isContainer } from '@qovery/shared/enums' -import { OrganizationEntity } from '@qovery/shared/interfaces' +import { ApplicationGeneralData, OrganizationEntity } from '@qovery/shared/interfaces' import { SERVICES_URL } from '@qovery/shared/router' import { Button, ButtonSize, ButtonStyle, Icon, InputSelect, InputText } from '@qovery/shared/ui' -import { GeneralData } from '../../../feature/page-application-create-feature/application-creation-flow.interface' -import CreateGeneralGitApplication from './create-general-git-application/create-general-git-application' export interface PageApplicationCreateGeneralProps { onSubmit: FormEventHandler @@ -15,11 +17,11 @@ export interface PageApplicationCreateGeneralProps { } export function PageApplicationCreateGeneral(props: PageApplicationCreateGeneralProps) { - const { control, getValues, watch, formState } = useFormContext() + const { control, watch, formState } = useFormContext() const { organizationId = '', environmentId = '', projectId = '' } = useParams() const navigate = useNavigate() - watch('serviceType') + const watchServiceType = watch('serviceType') return (
@@ -79,9 +81,14 @@ export function PageApplicationCreateGeneral(props: PageApplicationCreateGeneral />
- {isApplication(getValues().serviceType) && } + {isApplication(watchServiceType) && } - {isContainer(getValues().serviceType) && } + {isContainer(watchServiceType) && ( + <> + + + + )}
+ +
+ +
+ ) +} + +export default StepConfigure diff --git a/libs/pages/services/src/lib/ui/page-job-create/step-general/step-general.spec.tsx b/libs/pages/services/src/lib/ui/page-job-create/step-general/step-general.spec.tsx new file mode 100644 index 0000000000..83fbe518bd --- /dev/null +++ b/libs/pages/services/src/lib/ui/page-job-create/step-general/step-general.spec.tsx @@ -0,0 +1,25 @@ +import { render } from '__tests__/utils/setup-jest' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobGeneralData } from '@qovery/shared/interfaces' +import { StepGeneral, StepGeneralProps } from './step-general' + +const props: StepGeneralProps = { + jobType: ServiceTypeEnum.CRON_JOB, + onSubmit: jest.fn(), +} + +describe('General', () => { + it('should render successfully', () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues: { + name: 'test', + serviceType: ServiceTypeEnum.CONTAINER, + description: 'Application Description', + }, + }) + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/ui/page-job-create/step-general/step-general.tsx b/libs/pages/services/src/lib/ui/page-job-create/step-general/step-general.tsx new file mode 100644 index 0000000000..dfdb26735a --- /dev/null +++ b/libs/pages/services/src/lib/ui/page-job-create/step-general/step-general.tsx @@ -0,0 +1,96 @@ +import { FormEventHandler } from 'react' +import { Controller, useFormContext } from 'react-hook-form' +import { useNavigate, useParams } from 'react-router-dom' +import { JobGeneralSettings } from '@qovery/shared/console-shared' +import { JobType, ServiceTypeEnum } from '@qovery/shared/enums' +import { JobGeneralData, OrganizationEntity } from '@qovery/shared/interfaces' +import { SERVICES_URL } from '@qovery/shared/router' +import { Button, ButtonSize, ButtonStyle, InputText, InputTextArea } from '@qovery/shared/ui' + +export interface StepGeneralProps { + onSubmit: FormEventHandler + organization?: OrganizationEntity + jobType: JobType +} + +export function StepGeneral(props: StepGeneralProps) { + const { organizationId = '', environmentId = '', projectId = '' } = useParams() + const navigate = useNavigate() + const { formState, control } = useFormContext() + + return ( +
+
+

+ {props.jobType === ServiceTypeEnum.CRON_JOB ? 'Cron' : 'Lifecycle'} job informations +

+

+ General settings allow you to set up your application name, git repository or container settings. +

+
+ +

General

+ +
+ ( + + )} + /> + + ( + + )} + /> + + + +
+ + +
+ +
+ ) +} + +export default StepGeneral diff --git a/libs/pages/services/src/lib/ui/page-job-create/step-resources/step-resources.spec.tsx b/libs/pages/services/src/lib/ui/page-job-create/step-resources/step-resources.spec.tsx new file mode 100644 index 0000000000..5b7f35e5e3 --- /dev/null +++ b/libs/pages/services/src/lib/ui/page-job-create/step-resources/step-resources.spec.tsx @@ -0,0 +1,25 @@ +import ResizeObserver from '__tests__/utils/resize-observer' +import { render } from '__tests__/utils/setup-jest' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { JobResourcesData } from '@qovery/shared/interfaces' +import { StepResources, StepResourcesProps } from './step-resources' + +const props: StepResourcesProps = { + onSubmit: jest.fn(), + onBack: jest.fn(), +} + +describe('Resources', () => { + it('should render successfully', () => { + window.ResizeObserver = ResizeObserver + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues: { + cpu: [3], + memory: 1024, + }, + }) + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/ui/page-job-create/step-resources/step-resources.tsx b/libs/pages/services/src/lib/ui/page-job-create/step-resources/step-resources.tsx new file mode 100644 index 0000000000..329861ff26 --- /dev/null +++ b/libs/pages/services/src/lib/ui/page-job-create/step-resources/step-resources.tsx @@ -0,0 +1,50 @@ +import { FormEventHandler } from 'react' +import { useFormContext } from 'react-hook-form' +import { SettingResources } from '@qovery/shared/console-shared' +import { ApplicationResourcesData } from '@qovery/shared/interfaces' +import { Button, ButtonSize, ButtonStyle } from '@qovery/shared/ui' + +export interface StepResourcesProps { + onBack: () => void + onSubmit: FormEventHandler +} + +export function StepResources(props: StepResourcesProps) { + const { formState } = useFormContext() + + return ( + <> +
+

Set resources

+

Configure the resources required to run your job

+
+ +
+ + +
+ + +
+ + + ) +} + +export default StepResources diff --git a/libs/pages/services/src/lib/ui/page-job-create/step-summary/step-summary.spec.tsx b/libs/pages/services/src/lib/ui/page-job-create/step-summary/step-summary.spec.tsx new file mode 100644 index 0000000000..f41aefcbb4 --- /dev/null +++ b/libs/pages/services/src/lib/ui/page-job-create/step-summary/step-summary.spec.tsx @@ -0,0 +1,43 @@ +import { render } from '__tests__/utils/setup-jest' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { StepSummary, StepSummaryProps } from './step-summary' + +const props: StepSummaryProps = { + variableData: { + variables: [], + }, + generalData: { + name: 'test', + description: 'test', + serviceType: ServiceTypeEnum.CONTAINER, + }, + configureData: { + event: 'test', + cmd_arguments: 'test', + cmd: ['test'], + schedule: 'test', + nb_restarts: 1, + port: 3000, + image_entry_point: '/', + max_duration: 1, + }, + gotoGlobalInformation: jest.fn(), + gotoResources: jest.fn(), + gotoVariables: jest.fn(), + isLoadingCreate: false, + isLoadingCreateAndDeploy: false, + onPrevious: jest.fn(), + onSubmit: jest.fn(), + resourcesData: { + cpu: [3], + memory: 1024, + }, + selectedRegistryName: 'test', +} + +describe('Post', () => { + it('should render successfully', () => { + const { baseElement } = render() + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/pages/services/src/lib/ui/page-job-create/step-summary/step-summary.tsx b/libs/pages/services/src/lib/ui/page-job-create/step-summary/step-summary.tsx new file mode 100644 index 0000000000..208f340918 --- /dev/null +++ b/libs/pages/services/src/lib/ui/page-job-create/step-summary/step-summary.tsx @@ -0,0 +1,240 @@ +import { BuildModeEnum } from 'qovery-typescript-axios' +import { JobType, ServiceTypeEnum } from '@qovery/shared/enums' +import { FlowVariableData, JobConfigureData, JobGeneralData, JobResourcesData } from '@qovery/shared/interfaces' +import { Button, ButtonIcon, ButtonIconStyle, ButtonSize, ButtonStyle, Icon, IconAwesomeEnum } from '@qovery/shared/ui' + +export interface StepSummaryProps { + onSubmit: (withDeploy: boolean) => void + onPrevious: () => void + generalData: JobGeneralData + resourcesData: JobResourcesData + configureData: JobConfigureData + variableData: FlowVariableData + gotoGlobalInformation: () => void + gotoResources: () => void + gotoVariables: () => void + gotoConfigureJob: () => void + isLoadingCreate: boolean + isLoadingCreateAndDeploy: boolean + selectedRegistryName?: string + jobType: JobType +} + +export function StepSummary(props: StepSummaryProps) { + return ( +
+
+
+

Ready to create your Cronjob

+
+

+ The basic application setup is done, you can now deploy your application or move forward with some advanced + setup. +

+
+ +
+
+ +
+
General informations
+ +
General
+ +
    +
  • {props.generalData.name}
  • +
  • {props.generalData.description}
  • +
+ +
+ + {props.generalData.serviceType === ServiceTypeEnum.APPLICATION && ( + <> +
+ For application created from a Git provider +
+
    +
  • {props.generalData.repository}
  • +
  • {props.generalData.branch}
  • +
  • {props.generalData.root_path}
  • + {props.generalData.build_mode === BuildModeEnum.DOCKER && ( +
  • {props.generalData.dockerfile_path}
  • + )} +
+ + )} + {props.generalData.serviceType === ServiceTypeEnum.CONTAINER && ( + <> +
For application created from a Registry
+
    +
  • {props.selectedRegistryName}
  • +
  • Image name: {props.generalData.image_name}
  • +
  • Image tag: {props.generalData.image_tag}
  • +
  • Image entrypoint: {props.generalData.image_entry_point}
  • +
  • CMD arguments: {props.configureData.cmd_arguments}
  • +
+ + )} +
+ + +
+ +
+ +
+
Configure job
+ + {props.jobType === ServiceTypeEnum.LIFECYCLE_JOB && ( + <> +
Lifecycle job
+
    + {props.configureData.on_start?.enabled && ( +
  • + On start – Entrypoint: {props.configureData.on_start?.entrypoint || 'null'} / CMD:{' '} + {props.configureData.on_start?.arguments || 'null'} +
  • + )} + {props.configureData.on_stop?.enabled && ( +
  • + On Stop – Entrypoint: {props.configureData.on_stop?.entrypoint || 'null'} / CMD:{' '} + {props.configureData.on_stop?.arguments || 'null'} +
  • + )} + {props.configureData.on_delete?.enabled && ( +
  • + On Delete – Entrypoint: {props.configureData.on_delete?.entrypoint || 'null'} / CMD:{' '} + {props.configureData.on_delete?.arguments || 'null'} +
  • + )} +
+ + )} + + {props.jobType === ServiceTypeEnum.CRON_JOB && ( + <> +
CRON
+
    +
  • {props.configureData.schedule}
  • + {props.configureData.image_entry_point &&
  • {props.configureData.image_entry_point}
  • } + {props.configureData.cmd_arguments &&
  • {props.configureData.cmd_arguments}
  • } +
+ + )} + +
+ +
Parameters
+ +
    +
  • Max restarts: {props.configureData.nb_restarts}
  • +
  • Max duration: {props.configureData.max_duration}
  • +
  • Port: {props.configureData.port}
  • +
+
+ + +
+ +
+ +
+
Resources
+ +
Parameters
+
    +
  • CPU: {props.resourcesData['cpu'][0]}
  • +
  • Memory: {props.resourcesData.memory} MB
  • +
+
+ + +
+ +
+ +
+
Environment variables
+ +
+ Parameters{' '} + {props.variableData.variables && props.variableData.variables.length + ? `(${props.variableData.variables.length})` + : ''} +
+
    + {props.variableData.variables && props.variableData.variables.length > 0 ? ( + props.variableData.variables?.map((variable, index) => ( +
  • + {variable.variable} ={' '} + {variable.isSecret ? '********' : variable.value} – Secret:{' '} + {variable.isSecret ? 'Yes' : 'No'} +
  • + )) + ) : ( +
  • No variable declared
  • + )} +
+
+ + +
+
+ +
+ +
+ + +
+
+
+ ) +} + +export default StepSummary diff --git a/libs/shared/console-shared/src/index.ts b/libs/shared/console-shared/src/index.ts index 7937b80b95..e92eb8a784 100644 --- a/libs/shared/console-shared/src/index.ts +++ b/libs/shared/console-shared/src/index.ts @@ -1,10 +1,17 @@ -export * from './lib/environment-buttons-actions/environment-buttons-actions' +export * from './lib/job-configure-settings/ui/job-configure-settings' +export * from './lib/job-general-settings/ui/job-general-settings' +export * from './lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs' +export * from './lib/environment-buttons-actions/ui/environment-buttons-actions' export * from './lib/setting-resources/ui/setting-resources' export * from './lib/git-repository-settings/feature/git-repository-settings-feature' export * from './lib/git-repository-settings/ui/git-repository-settings' export * from './lib/general-container-settings/ui/general-container-settings' export * from './lib/deploy-other-commit-modal/feature/deploy-other-commit-modal-feature' -export * from './lib/application-buttons-actions/application-buttons-actions' -export * from './lib/database-buttons-actions/database-buttons-actions' -export * from './lib/cluster-buttons-actions/cluster-buttons-actions' +export * from './lib/application-buttons-actions/ui/application-buttons-actions' +export * from './lib/database-buttons-actions/ui/database-buttons-actions' +export * from './lib/cluster-buttons-actions/ui/cluster-buttons-actions' export * from './lib/create-clone-environment-modal/feature/create-clone-environment-modal-feature' +export * from './lib/flow-create-port/ui/flow-create-port' +export * from './lib/flow-create-variable/ui/flow-create-variable' +export * from './lib/create-general-git-application/ui/create-general-git-application' +export * from './lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature' diff --git a/libs/shared/console-shared/src/lib/application-buttons-actions/application-buttons-actions.spec.tsx b/libs/shared/console-shared/src/lib/application-buttons-actions/ui/application-buttons-actions.spec.tsx similarity index 100% rename from libs/shared/console-shared/src/lib/application-buttons-actions/application-buttons-actions.spec.tsx rename to libs/shared/console-shared/src/lib/application-buttons-actions/ui/application-buttons-actions.spec.tsx diff --git a/libs/shared/console-shared/src/lib/application-buttons-actions/application-buttons-actions.tsx b/libs/shared/console-shared/src/lib/application-buttons-actions/ui/application-buttons-actions.tsx similarity index 98% rename from libs/shared/console-shared/src/lib/application-buttons-actions/application-buttons-actions.tsx rename to libs/shared/console-shared/src/lib/application-buttons-actions/ui/application-buttons-actions.tsx index 19501d60b2..bb30f65aaa 100644 --- a/libs/shared/console-shared/src/lib/application-buttons-actions/application-buttons-actions.tsx +++ b/libs/shared/console-shared/src/lib/application-buttons-actions/ui/application-buttons-actions.tsx @@ -40,7 +40,7 @@ import { urlCodeEditor, } from '@qovery/shared/utils' import { AppDispatch } from '@qovery/store' -import DeployOtherCommitModalFeature from '../deploy-other-commit-modal/feature/deploy-other-commit-modal-feature' +import DeployOtherCommitModalFeature from '../../deploy-other-commit-modal/feature/deploy-other-commit-modal-feature' export interface ApplicationButtonsActionsProps { application: ApplicationEntity diff --git a/libs/shared/console-shared/src/lib/cluster-buttons-actions/cluster-buttons-actions.spec.tsx b/libs/shared/console-shared/src/lib/cluster-buttons-actions/ui/cluster-buttons-actions.spec.tsx similarity index 100% rename from libs/shared/console-shared/src/lib/cluster-buttons-actions/cluster-buttons-actions.spec.tsx rename to libs/shared/console-shared/src/lib/cluster-buttons-actions/ui/cluster-buttons-actions.spec.tsx diff --git a/libs/shared/console-shared/src/lib/cluster-buttons-actions/cluster-buttons-actions.tsx b/libs/shared/console-shared/src/lib/cluster-buttons-actions/ui/cluster-buttons-actions.tsx similarity index 100% rename from libs/shared/console-shared/src/lib/cluster-buttons-actions/cluster-buttons-actions.tsx rename to libs/shared/console-shared/src/lib/cluster-buttons-actions/ui/cluster-buttons-actions.tsx diff --git a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/create-general-git-application/create-general-git-application.spec.tsx b/libs/shared/console-shared/src/lib/create-general-git-application/ui/create-general-git-application.spec.tsx similarity index 100% rename from libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/create-general-git-application/create-general-git-application.spec.tsx rename to libs/shared/console-shared/src/lib/create-general-git-application/ui/create-general-git-application.spec.tsx diff --git a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/create-general-git-application/create-general-git-application.tsx b/libs/shared/console-shared/src/lib/create-general-git-application/ui/create-general-git-application.tsx similarity index 87% rename from libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/create-general-git-application/create-general-git-application.tsx rename to libs/shared/console-shared/src/lib/create-general-git-application/ui/create-general-git-application.tsx index 38a1cc17a2..1c5f0f52fe 100644 --- a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-general/create-general-git-application/create-general-git-application.tsx +++ b/libs/shared/console-shared/src/lib/create-general-git-application/ui/create-general-git-application.tsx @@ -1,12 +1,16 @@ import { BuildModeEnum, BuildPackLanguageEnum } from 'qovery-typescript-axios' import { Controller, useFormContext } from 'react-hook-form' import { GitRepositorySettingsFeature } from '@qovery/shared/console-shared' +import { ApplicationGeneralData } from '@qovery/shared/interfaces' import { Icon, InputSelect, InputText } from '@qovery/shared/ui' import { upperCaseFirstLetter } from '@qovery/shared/utils' -import { GeneralData } from '../../../../feature/page-application-create-feature/application-creation-flow.interface' -export function CreateGeneralGitApplication() { - const { control, watch } = useFormContext() +export interface PageSettingsGeneralProps { + buildModeDisabled?: boolean +} + +export function CreateGeneralGitApplication(props: PageSettingsGeneralProps) { + const { control, watch } = useFormContext() const watchBuildMode = watch('build_mode') const buildModeItems = Object.values(BuildModeEnum).map((value) => ({ @@ -22,9 +26,8 @@ export function CreateGeneralGitApplication() { return ( <> -

- For Applications created from a Git Provider, fill the informations below -

+

For applications created from a Git Provider

+
@@ -44,6 +47,7 @@ export function CreateGeneralGitApplication() { dataTestId="input-select-mode" label="Mode" className="mb-3" + disabled={props.buildModeDisabled} options={buildModeItems} onChange={field.onChange} value={field.value} diff --git a/libs/shared/console-shared/src/lib/database-buttons-actions/database-buttons-actions.spec.tsx b/libs/shared/console-shared/src/lib/database-buttons-actions/ui/database-buttons-actions.spec.tsx similarity index 100% rename from libs/shared/console-shared/src/lib/database-buttons-actions/database-buttons-actions.spec.tsx rename to libs/shared/console-shared/src/lib/database-buttons-actions/ui/database-buttons-actions.spec.tsx diff --git a/libs/shared/console-shared/src/lib/database-buttons-actions/database-buttons-actions.tsx b/libs/shared/console-shared/src/lib/database-buttons-actions/ui/database-buttons-actions.tsx similarity index 100% rename from libs/shared/console-shared/src/lib/database-buttons-actions/database-buttons-actions.tsx rename to libs/shared/console-shared/src/lib/database-buttons-actions/ui/database-buttons-actions.tsx diff --git a/libs/shared/console-shared/src/lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs.spec.tsx b/libs/shared/console-shared/src/lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs.spec.tsx new file mode 100644 index 0000000000..b984bd293c --- /dev/null +++ b/libs/shared/console-shared/src/lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs.spec.tsx @@ -0,0 +1,13 @@ +import { getByTestId } from '@testing-library/react' +import { render } from '__tests__/utils/setup-jest' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import EntrypointCmdInputs from './entrypoint-cmd-inputs' + +describe('EntrypointCmdInputs', () => { + it('should render successfully', () => { + const { baseElement } = render(wrapWithReactHookForm()) + getByTestId(baseElement, 'input-text-image-entry-point') + getByTestId(baseElement, 'input-textarea-cmd-arguments') + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/shared/console-shared/src/lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs.tsx b/libs/shared/console-shared/src/lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs.tsx new file mode 100644 index 0000000000..0fc1b0c574 --- /dev/null +++ b/libs/shared/console-shared/src/lib/entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs.tsx @@ -0,0 +1,66 @@ +import { Controller, useFormContext } from 'react-hook-form' +import { InputText, InputTextArea } from '@qovery/shared/ui' + +export interface EntrypointCmdInputsProps { + className?: string + entrypointRequired?: boolean + cmdRequired?: boolean + imageEntryPointFieldName?: string + cmdArgumentsFieldName?: string +} + +export function EntrypointCmdInputs(props: EntrypointCmdInputsProps) { + const { + className = 'mb-6', + entrypointRequired = false, + cmdRequired = false, + imageEntryPointFieldName = 'image_entry_point', + cmdArgumentsFieldName = 'cmd_arguments', + } = props + const { control } = useFormContext() + + return ( +
+ ( + + )} + /> + ( + + )} + /> +

+ Expected format: ["rails", "-h", "0.0.0.0", "-p", "8080", "string"] +

+
+ ) +} + +export default EntrypointCmdInputs diff --git a/libs/shared/console-shared/src/lib/environment-buttons-actions/environment-buttons-actions.spec.tsx b/libs/shared/console-shared/src/lib/environment-buttons-actions/ui/environment-buttons-actions.spec.tsx similarity index 100% rename from libs/shared/console-shared/src/lib/environment-buttons-actions/environment-buttons-actions.spec.tsx rename to libs/shared/console-shared/src/lib/environment-buttons-actions/ui/environment-buttons-actions.spec.tsx diff --git a/libs/shared/console-shared/src/lib/environment-buttons-actions/environment-buttons-actions.tsx b/libs/shared/console-shared/src/lib/environment-buttons-actions/ui/environment-buttons-actions.tsx similarity index 98% rename from libs/shared/console-shared/src/lib/environment-buttons-actions/environment-buttons-actions.tsx rename to libs/shared/console-shared/src/lib/environment-buttons-actions/ui/environment-buttons-actions.tsx index 727150ebc8..f1d390286a 100644 --- a/libs/shared/console-shared/src/lib/environment-buttons-actions/environment-buttons-actions.tsx +++ b/libs/shared/console-shared/src/lib/environment-buttons-actions/ui/environment-buttons-actions.tsx @@ -36,7 +36,7 @@ import { isStopAvailable, } from '@qovery/shared/utils' import { AppDispatch } from '@qovery/store' -import CreateCloneEnvironmentModalFeature from '../create-clone-environment-modal/feature/create-clone-environment-modal-feature' +import CreateCloneEnvironmentModalFeature from '../../create-clone-environment-modal/feature/create-clone-environment-modal-feature' export interface EnvironmentButtonsActionsProps { environment: EnvironmentEntity diff --git a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/port-row/port-row.spec.tsx b/libs/shared/console-shared/src/lib/flow-create-port/port-row/port-row.spec.tsx similarity index 92% rename from libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/port-row/port-row.spec.tsx rename to libs/shared/console-shared/src/lib/flow-create-port/port-row/port-row.spec.tsx index e2c95bfe7a..f385b704f1 100644 --- a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/port-row/port-row.spec.tsx +++ b/libs/shared/console-shared/src/lib/flow-create-port/port-row/port-row.spec.tsx @@ -1,6 +1,6 @@ import { act, getAllByTestId, getByTestId } from '@testing-library/react' import { render } from '__tests__/utils/setup-jest' -import { wrapWithReactHookForm } from '../../../../../../../../../__tests__/utils/wrap-with-react-hook-form' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' import PortRow, { PortRowProps } from './port-row' const props: PortRowProps = { diff --git a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/port-row/port-row.tsx b/libs/shared/console-shared/src/lib/flow-create-port/port-row/port-row.tsx similarity index 95% rename from libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/port-row/port-row.tsx rename to libs/shared/console-shared/src/lib/flow-create-port/port-row/port-row.tsx index 820e11b29c..ff43e267af 100644 --- a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/port-row/port-row.tsx +++ b/libs/shared/console-shared/src/lib/flow-create-port/port-row/port-row.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react' import { Controller, useFormContext } from 'react-hook-form' +import { FlowPortData } from '@qovery/shared/interfaces' import { ButtonIcon, ButtonIconStyle, Icon, IconAwesomeEnum, InputText, InputToggle, Tooltip } from '@qovery/shared/ui' -import { PortData } from '../../../../feature/page-application-create-feature/application-creation-flow.interface' export interface PortRowProps { index: number @@ -10,7 +10,7 @@ export interface PortRowProps { export function PortRow(props: PortRowProps) { const { index } = props - const { control, watch, setValue, resetField } = useFormContext() + const { control, watch, setValue, resetField } = useFormContext() const isPublicWatch = watch(`ports.${index}.is_public`) const externalPortWatch = watch(`ports.${index}.external_port`) diff --git a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/page-application-create-port.spec.tsx b/libs/shared/console-shared/src/lib/flow-create-port/ui/flow-create-port.spec.tsx similarity index 68% rename from libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/page-application-create-port.spec.tsx rename to libs/shared/console-shared/src/lib/flow-create-port/ui/flow-create-port.spec.tsx index b007295244..1aa610ef80 100644 --- a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/page-application-create-port.spec.tsx +++ b/libs/shared/console-shared/src/lib/flow-create-port/ui/flow-create-port.spec.tsx @@ -1,9 +1,9 @@ import { act } from '@testing-library/react' import { render } from '__tests__/utils/setup-jest' import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' -import PageApplicationCreatePort, { PageApplicationCreatePortProps } from './page-application-create-port' +import FlowCreatePort, { FlowCreatePortProps } from './flow-create-port' -const props: PageApplicationCreatePortProps = { +const props: FlowCreatePortProps = { onSubmit: jest.fn(), onBack: jest.fn(), onAddPort: jest.fn(), @@ -24,18 +24,18 @@ const props: PageApplicationCreatePortProps = { describe('PageApplicationCreatePort', () => { it('should render successfully', () => { - const { baseElement } = render(wrapWithReactHookForm()) + const { baseElement } = render(wrapWithReactHookForm()) expect(baseElement).toBeTruthy() }) it('should render two rows', () => { - const { getAllByTestId } = render(wrapWithReactHookForm()) + const { getAllByTestId } = render(wrapWithReactHookForm()) expect(getAllByTestId('port-row')).toHaveLength(2) }) it('should submit the form', async () => { const { getAllByTestId } = render( - wrapWithReactHookForm(, { defaultValues: { ports: props.ports } }) + wrapWithReactHookForm(, { defaultValues: { ports: props.ports } }) ) await act(() => {}) diff --git a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/page-application-create-port.tsx b/libs/shared/console-shared/src/lib/flow-create-port/ui/flow-create-port.tsx similarity index 78% rename from libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/page-application-create-port.tsx rename to libs/shared/console-shared/src/lib/flow-create-port/ui/flow-create-port.tsx index f3d61b4134..2cdd78fb9e 100644 --- a/libs/pages/services/src/lib/ui/page-application-create/page-application-create-port/page-application-create-port.tsx +++ b/libs/shared/console-shared/src/lib/flow-create-port/ui/flow-create-port.tsx @@ -1,19 +1,19 @@ import { FormEventHandler } from 'react' import { useFormContext } from 'react-hook-form' +import { FlowPortData, PortData } from '@qovery/shared/interfaces' import { Button, ButtonSize, ButtonStyle } from '@qovery/shared/ui' -import { PortData } from '../../../feature/page-application-create-feature/application-creation-flow.interface' -import PortRow from './port-row/port-row' +import PortRow from '../port-row/port-row' -export interface PageApplicationCreatePortProps { +export interface FlowCreatePortProps { onBack: () => void onSubmit: FormEventHandler onAddPort: () => void onRemovePort: (index: number) => void - ports: { application_port: number | undefined; external_port: number | undefined; is_public: boolean }[] + ports: PortData[] } -export function PageApplicationCreatePort(props: PageApplicationCreatePortProps) { - const { formState } = useFormContext() +export function FlowCreatePort(props: FlowCreatePortProps) { + const { formState } = useFormContext() return (
@@ -64,4 +64,4 @@ export function PageApplicationCreatePort(props: PageApplicationCreatePortProps) ) } -export default PageApplicationCreatePort +export default FlowCreatePort diff --git a/libs/shared/console-shared/src/lib/flow-create-variable/ui/flow-create-variable.spec.tsx b/libs/shared/console-shared/src/lib/flow-create-variable/ui/flow-create-variable.spec.tsx new file mode 100644 index 0000000000..9152ab7e74 --- /dev/null +++ b/libs/shared/console-shared/src/lib/flow-create-variable/ui/flow-create-variable.spec.tsx @@ -0,0 +1,27 @@ +import { render } from '@testing-library/react' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { APIVariableScopeEnum } from 'qovery-typescript-axios' +import { FlowVariableData } from '@qovery/shared/interfaces' +import FlowCreateVariable, { FlowCreateVariableProps } from './flow-create-variable' + +const props: FlowCreateVariableProps = { + onBack: jest.fn(), + availableScopes: [APIVariableScopeEnum.PROJECT, APIVariableScopeEnum.CONTAINER, APIVariableScopeEnum.APPLICATION], + variables: [], + onSubmit: jest.fn(), + onRemove: jest.fn(), + onAdd: jest.fn(), +} + +describe('FlowCreateVariable', () => { + it('should render successfully', () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues: { + variables: [], + }, + }) + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/shared/console-shared/src/lib/flow-create-variable/ui/flow-create-variable.tsx b/libs/shared/console-shared/src/lib/flow-create-variable/ui/flow-create-variable.tsx new file mode 100644 index 0000000000..091951273c --- /dev/null +++ b/libs/shared/console-shared/src/lib/flow-create-variable/ui/flow-create-variable.tsx @@ -0,0 +1,82 @@ +import { APIVariableScopeEnum } from 'qovery-typescript-axios' +import { FormEventHandler } from 'react' +import { useFormContext } from 'react-hook-form' +import { FlowVariableData, VariableData } from '@qovery/shared/interfaces' +import { Button, ButtonSize, ButtonStyle } from '@qovery/shared/ui' +import VariableRow from '../variable-row/variable-row' + +export interface FlowCreateVariableProps { + onBack: () => void + onSubmit: FormEventHandler + onAdd: () => void + onRemove: (index: number) => void + variables: VariableData[] + availableScopes: APIVariableScopeEnum[] +} + +export function FlowCreateVariable(props: FlowCreateVariableProps) { + const { formState } = useFormContext() + const gridTemplateColumns = '6fr 6fr 204px 2fr 1fr' + + return ( +
+
+
+

Set environment variables

+ +
+ +

Define here the variables required by your job

+
+ +
+ {props.variables?.length > 0 && ( +
+ Variable + Value + Scope + Secret + +
+ )} + +
+ {props.variables?.map((_, index) => ( + + ))} +
+ +
+ + +
+
+
+ ) +} + +export default FlowCreateVariable diff --git a/libs/shared/console-shared/src/lib/flow-create-variable/variable-row/variable-row.spec.tsx b/libs/shared/console-shared/src/lib/flow-create-variable/variable-row/variable-row.spec.tsx new file mode 100644 index 0000000000..850dc49e03 --- /dev/null +++ b/libs/shared/console-shared/src/lib/flow-create-variable/variable-row/variable-row.spec.tsx @@ -0,0 +1,25 @@ +import { render } from '__tests__/utils/setup-jest' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { APIVariableScopeEnum } from 'qovery-typescript-axios' +import { FlowVariableData } from '@qovery/shared/interfaces' +import VariableRow, { VariableRowProps } from './variable-row' + +const props: VariableRowProps = { + index: 0, + onDelete: jest.fn(), + gridTemplateColumns: '', + availableScopes: [APIVariableScopeEnum.PROJECT, APIVariableScopeEnum.CONTAINER, APIVariableScopeEnum.APPLICATION], +} + +describe('VariableRow', () => { + it('should render successfully', () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues: { + variables: [], + }, + }) + ) + expect(baseElement).toBeTruthy() + }) +}) diff --git a/libs/shared/console-shared/src/lib/flow-create-variable/variable-row/variable-row.tsx b/libs/shared/console-shared/src/lib/flow-create-variable/variable-row/variable-row.tsx new file mode 100644 index 0000000000..2283a87603 --- /dev/null +++ b/libs/shared/console-shared/src/lib/flow-create-variable/variable-row/variable-row.tsx @@ -0,0 +1,115 @@ +import { APIVariableScopeEnum } from 'qovery-typescript-axios' +import { Controller, useFormContext } from 'react-hook-form' +import { FlowVariableData } from '@qovery/shared/interfaces' +import { + ButtonIcon, + ButtonIconStyle, + ButtonSize, + IconAwesomeEnum, + InputSelectSmall, + InputTextSmall, + InputToggle, +} from '@qovery/shared/ui' + +export interface VariableRowProps { + index: number + onDelete: (index: number) => void + availableScopes: APIVariableScopeEnum[] + gridTemplateColumns?: string +} + +export function VariableRow(props: VariableRowProps) { + const { index, availableScopes, gridTemplateColumns = '6fr 6fr 204px 2fr 1fr 1fr' } = props + const { control, trigger } = useFormContext() + + const pattern = /^[^\s]+$/ + + return ( +
+
+ ( + + )} + /> + + ( + + )} + /> + + ( + { + field.onChange(e) + trigger(`variables.${index}.value`).then() + }} + items={availableScopes.map((s) => ({ value: s, label: s.toLowerCase() }))} + /> + )} + /> + +
+ } + /> +
+ +
+ props.onDelete(index)} + className="text-text-400 hover:text-text-500 !w-8 !h-8" + iconClassName="!text-xs" + /> +
+
+
+ ) +} + +export default VariableRow diff --git a/libs/shared/console-shared/src/lib/general-container-settings/ui/general-container-settings.spec.tsx b/libs/shared/console-shared/src/lib/general-container-settings/ui/general-container-settings.spec.tsx index 77eccea15d..3c5ac76788 100644 --- a/libs/shared/console-shared/src/lib/general-container-settings/ui/general-container-settings.spec.tsx +++ b/libs/shared/console-shared/src/lib/general-container-settings/ui/general-container-settings.spec.tsx @@ -14,7 +14,5 @@ describe('CreateGeneralContainer', () => { getByTestId(baseElement, 'input-select-registry') getByTestId(baseElement, 'input-text-image-name') getByTestId(baseElement, 'input-text-image-tag') - getByTestId(baseElement, 'input-text-image-entry-point') - getByTestId(baseElement, 'input-textarea-cmd-arguments') }) }) diff --git a/libs/shared/console-shared/src/lib/general-container-settings/ui/general-container-settings.tsx b/libs/shared/console-shared/src/lib/general-container-settings/ui/general-container-settings.tsx index 1d1b4a96dc..8900af2fc0 100644 --- a/libs/shared/console-shared/src/lib/general-container-settings/ui/general-container-settings.tsx +++ b/libs/shared/console-shared/src/lib/general-container-settings/ui/general-container-settings.tsx @@ -2,38 +2,36 @@ import { useEffect, useState } from 'react' import { Controller, useFormContext } from 'react-hook-form' import { OrganizationEntity, Value } from '@qovery/shared/interfaces' import { SETTINGS_CONTAINER_REGISTRIES_URL, SETTINGS_URL } from '@qovery/shared/router' -import { InputSelect, InputText, InputTextArea, Link } from '@qovery/shared/ui' +import { InputSelect, InputText, Link } from '@qovery/shared/ui' export interface GeneralContainerSettingsProps { organization?: OrganizationEntity + className?: string } export function GeneralContainerSettings(props: GeneralContainerSettingsProps) { + const { organization, className = 'mb-6' } = props const { control } = useFormContext<{ registry?: string image_name?: string image_tag?: string - image_entry_point?: string - cmd_arguments?: string }>() const [availableRegistiesOptions, setAvailableRegistiesOptions] = useState([]) useEffect(() => { - if (props.organization?.containerRegistries?.items && props.organization.containerRegistries.items.length > 0) { + if (organization?.containerRegistries?.items && organization.containerRegistries.items.length > 0) { setAvailableRegistiesOptions( - props.organization.containerRegistries.items.map((registry) => ({ + organization.containerRegistries.items.map((registry) => ({ value: registry.id, label: registry.name || '', })) ) } - }, [props.organization]) + }, [organization]) return ( -
-

- For Applications created from a Registry, fill the informations below -

+
+

For applications created from a registry

@@ -96,38 +94,6 @@ export function GeneralContainerSettings(props: GeneralContainerSettingsProps) { /> )} /> - ( - - )} - /> - ( - - )} - /> -

- Expected format: ["rails", "-h", "0.0.0.0", "-p", "8080", "string"] -

) } diff --git a/libs/shared/console-shared/src/lib/git-repository-settings/auth-providers-values.tsx b/libs/shared/console-shared/src/lib/git-repository-settings/auth-providers-values.tsx new file mode 100644 index 0000000000..fbcbf6723e --- /dev/null +++ b/libs/shared/console-shared/src/lib/git-repository-settings/auth-providers-values.tsx @@ -0,0 +1,11 @@ +import { GitAuthProvider } from 'qovery-typescript-axios' +import { Icon } from '@qovery/shared/ui' +import { upperCaseFirstLetter } from '@qovery/shared/utils' + +export const authProvidersValues = (authProviders: GitAuthProvider[]) => { + return authProviders.map((provider: GitAuthProvider) => ({ + label: `${upperCaseFirstLetter(provider.name)} (${provider.owner})`, + value: provider.name || '', + icon: , + })) +} diff --git a/libs/pages/application/src/lib/feature/git-repository-settings-feature/git-repository-settings-feature.spec.tsx b/libs/shared/console-shared/src/lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature.spec.tsx similarity index 75% rename from libs/pages/application/src/lib/feature/git-repository-settings-feature/git-repository-settings-feature.spec.tsx rename to libs/shared/console-shared/src/lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature.spec.tsx index b2a70b929c..02235ccd6d 100644 --- a/libs/pages/application/src/lib/feature/git-repository-settings-feature/git-repository-settings-feature.spec.tsx +++ b/libs/shared/console-shared/src/lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature.spec.tsx @@ -2,11 +2,12 @@ import { render } from '__tests__/utils/setup-jest' import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' import { authProviderFactoryMock } from '@qovery/domains/organization' import { upperCaseFirstLetter } from '@qovery/shared/utils' -import GitRepositorySettingsFeature, { authProvidersValues } from './git-repository-settings-feature' +import { authProvidersValues } from '../auth-providers-values' +import EditGitRepositorySettingsFeature from './edit-git-repository-settings-feature' describe('GitRepositorySettingsFeature', () => { it('should render successfully', () => { - const { baseElement } = render(wrapWithReactHookForm()) + const { baseElement } = render(wrapWithReactHookForm()) expect(baseElement).toBeTruthy() }) diff --git a/libs/pages/application/src/lib/feature/git-repository-settings-feature/git-repository-settings-feature.tsx b/libs/shared/console-shared/src/lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature.tsx similarity index 60% rename from libs/pages/application/src/lib/feature/git-repository-settings-feature/git-repository-settings-feature.tsx rename to libs/shared/console-shared/src/lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature.tsx index f6af91c5ea..9c0ac12444 100644 --- a/libs/pages/application/src/lib/feature/git-repository-settings-feature/git-repository-settings-feature.tsx +++ b/libs/shared/console-shared/src/lib/git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature.tsx @@ -1,9 +1,9 @@ import { GitAuthProvider, GitProviderEnum } from 'qovery-typescript-axios' -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import { useFormContext } from 'react-hook-form' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' -import { getApplicationsState } from '@qovery/domains/application' +import { selectApplicationById } from '@qovery/domains/application' import { authProviderLoadingStatus, fetchAuthProvider, @@ -13,29 +13,33 @@ import { selectAllAuthProvider, selectRepositoriesByProvider, } from '@qovery/domains/organization' -import { GitRepositorySettings } from '@qovery/shared/console-shared' -import { GitApplicationEntity, LoadingStatus, RepositoryEntity } from '@qovery/shared/interfaces' +import { isJob } from '@qovery/shared/enums' +import { GitApplicationEntity, JobApplicationEntity, LoadingStatus, RepositoryEntity } from '@qovery/shared/interfaces' import { Icon } from '@qovery/shared/ui' import { upperCaseFirstLetter } from '@qovery/shared/utils' import { AppDispatch, RootState } from '@qovery/store' +import { authProvidersValues } from '../auth-providers-values' +import GitRepositorySettings from '../ui/git-repository-settings' -export const authProvidersValues = (authProviders: GitAuthProvider[]) => { - return authProviders.map((provider: GitAuthProvider) => ({ - label: `${upperCaseFirstLetter(provider.name)} (${provider.owner})`, - value: provider.name || '', - icon: , - })) -} - -export function GitRepositorySettingsFeature() { +export function EditGitRepositorySettingsFeature() { const { organizationId = '', applicationId = '' } = useParams() const dispatch = useDispatch() - const application = useSelector( - (state) => getApplicationsState(state).entities[applicationId], - (a, b) => JSON.stringify(a?.git_repository) === JSON.stringify(b?.git_repository) + const application = useSelector( + (state) => selectApplicationById(state, applicationId), + (a, b) => + JSON.stringify((a as GitApplicationEntity)?.git_repository) === + JSON.stringify((b as GitApplicationEntity)?.git_repository) || + JSON.stringify((a as JobApplicationEntity)?.source?.docker?.git_repository) === + JSON.stringify((b as JobApplicationEntity)?.source?.docker?.git_repository) ) + const getGitRepositoryFromApplication = useCallback(() => { + return isJob(application) + ? (application as JobApplicationEntity).source?.docker?.git_repository + : (application as GitApplicationEntity)?.git_repository + }, [application]) + const { setValue, watch, getValues } = useFormContext<{ provider: string repository: string | undefined @@ -54,8 +58,8 @@ export function GitRepositorySettingsFeature() { const [gitDisabled, setGitDisabled] = useState(true) - const currentAuthProvider = `${upperCaseFirstLetter(application?.git_repository?.provider)} (${ - application?.git_repository?.owner + const currentAuthProvider = `${upperCaseFirstLetter(getGitRepositoryFromApplication()?.provider)} (${ + getGitRepositoryFromApplication()?.owner })` const currentRepository = repositories.find((repository) => repository.name === watchRepository) @@ -66,13 +70,16 @@ export function GitRepositorySettingsFeature() { }, [dispatch, organizationId, watchAuthProvider]) useEffect(() => { - if (gitDisabled && application?.git_repository) { - setValue('provider', `${application?.git_repository?.provider} (${application?.git_repository?.owner})`) - setValue('repository', application?.git_repository?.url) - setValue('branch', application?.git_repository?.branch) - setValue('root_path', application?.git_repository?.root_path) + if (gitDisabled && getGitRepositoryFromApplication()) { + setValue( + 'provider', + `${getGitRepositoryFromApplication()?.provider} (${getGitRepositoryFromApplication()?.owner})` + ) + setValue('repository', getGitRepositoryFromApplication()?.url) + setValue('branch', getGitRepositoryFromApplication()?.branch) + setValue('root_path', getGitRepositoryFromApplication()?.root_path) } - }, [application?.git_repository, setValue, gitDisabled, authProviders, repositories]) + }, [getGitRepositoryFromApplication, setValue, gitDisabled, authProviders, repositories]) // fetch branches by repository and set default branch useEffect(() => { @@ -94,13 +101,13 @@ export function GitRepositorySettingsFeature() { const editGitSettings = () => { setGitDisabled(false) dispatch(fetchAuthProvider({ organizationId })) - if (application?.git_repository?.provider) { - setValue('provider', application?.git_repository?.provider) + if (getGitRepositoryFromApplication()?.provider) { + setValue('provider', getGitRepositoryFromApplication()?.provider || '') setValue('repository', undefined, { shouldValidate: false }) } } - if (!application?.git_repository?.name) return null + if (!getGitRepositoryFromApplication()?.name) return null return ( , + value: `${getGitRepositoryFromApplication()?.provider} (${getGitRepositoryFromApplication()?.owner})`, + icon: , }, ] } @@ -128,18 +135,18 @@ export function GitRepositorySettingsFeature() { })) : [ { - label: upperCaseFirstLetter(application?.git_repository?.name) || '', - value: application?.git_repository?.url || '', + label: upperCaseFirstLetter(getGitRepositoryFromApplication()?.name) || '', + value: getGitRepositoryFromApplication()?.url || '', }, ] } loadingStatusBranches={!currentRepository ? 'not loaded' : currentRepository?.branches?.loadingStatus} branches={ - gitDisabled && application?.git_repository?.branch + gitDisabled && getGitRepositoryFromApplication()?.branch ? [ { - label: application?.git_repository?.branch, - value: application?.git_repository?.branch, + label: getGitRepositoryFromApplication()?.branch || '', + value: getGitRepositoryFromApplication()?.branch || '', }, ] : currentRepository?.branches?.items @@ -153,4 +160,4 @@ export function GitRepositorySettingsFeature() { ) } -export default GitRepositorySettingsFeature +export default EditGitRepositorySettingsFeature diff --git a/libs/shared/console-shared/src/lib/git-repository-settings/feature/git-repository-settings-feature.tsx b/libs/shared/console-shared/src/lib/git-repository-settings/feature/git-repository-settings-feature.tsx index 840c822a22..bd56fc632d 100644 --- a/libs/shared/console-shared/src/lib/git-repository-settings/feature/git-repository-settings-feature.tsx +++ b/libs/shared/console-shared/src/lib/git-repository-settings/feature/git-repository-settings-feature.tsx @@ -13,23 +13,15 @@ import { selectRepositoriesByProvider, } from '@qovery/domains/organization' import { LoadingStatus, RepositoryEntity } from '@qovery/shared/interfaces' -import { Icon } from '@qovery/shared/ui' import { upperCaseFirstLetter } from '@qovery/shared/utils' import { AppDispatch, RootState } from '@qovery/store' +import { authProvidersValues } from '../auth-providers-values' import GitRepositorySettings from '../ui/git-repository-settings' export interface GitRepositorySettingsFeatureProps { withBlockWrapper?: boolean } -export const authProvidersValues = (authProviders: GitAuthProvider[]) => { - return authProviders.map((provider: GitAuthProvider) => ({ - label: `${upperCaseFirstLetter(provider.name)} (${provider.owner})`, - value: provider.name || '', - icon: , - })) -} - export function GitRepositorySettingsFeature(props?: GitRepositorySettingsFeatureProps) { const { organizationId = '' } = useParams() const dispatch = useDispatch() diff --git a/libs/shared/console-shared/src/lib/git-repository-settings/ui/git-repository-settings.spec.tsx b/libs/shared/console-shared/src/lib/git-repository-settings/ui/git-repository-settings.spec.tsx index f021a7e0f7..5ca0689f8d 100644 --- a/libs/shared/console-shared/src/lib/git-repository-settings/ui/git-repository-settings.spec.tsx +++ b/libs/shared/console-shared/src/lib/git-repository-settings/ui/git-repository-settings.spec.tsx @@ -3,7 +3,7 @@ import { render } from '__tests__/utils/setup-jest' import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' import { GitProviderEnum } from 'qovery-typescript-axios' import { authProviderFactoryMock } from '@qovery/domains/organization' -import { authProvidersValues } from '../feature/git-repository-settings-feature' +import { authProvidersValues } from '../auth-providers-values' import { GitRepositorySettings, GitRepositorySettingsProps } from './git-repository-settings' const mockOpenModal = jest.fn() diff --git a/libs/shared/console-shared/src/lib/job-configure-settings/ui/job-configure-settings.spec.tsx b/libs/shared/console-shared/src/lib/job-configure-settings/ui/job-configure-settings.spec.tsx new file mode 100644 index 0000000000..ab7ca8c3ad --- /dev/null +++ b/libs/shared/console-shared/src/lib/job-configure-settings/ui/job-configure-settings.spec.tsx @@ -0,0 +1,82 @@ +import { act, fireEvent, getAllByTestId, getByLabelText, getByTestId, getByText, render } from '@testing-library/react' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobConfigureData } from '@qovery/shared/interfaces' +import JobConfigureSettings, { JobConfigureSettingsProps } from './job-configure-settings' + +const props: JobConfigureSettingsProps = { + jobType: ServiceTypeEnum.CRON_JOB, +} + +const defaultValues: JobConfigureData = { + port: 80, + cmd: ['test'], + nb_restarts: 0, + schedule: '0 0 * * *', + cmd_arguments: "['test']", + event: 'test', + image_entry_point: 'test', + max_duration: 0, +} + +describe('JobConfigureSettings', () => { + it('should render successfully', () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues, + }) + ) + expect(baseElement).toBeTruthy() + }) + + describe('job is a lifecycle', () => { + props.jobType = ServiceTypeEnum.LIFECYCLE_JOB + + it('should render 3 enabled box and 3 inputs', () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues, + }) + ) + + expect(getAllByTestId(baseElement, 'input-text')).toHaveLength(3) + expect(getAllByTestId(baseElement, 'enabled-box')).toHaveLength(3) + expect(baseElement).toBeTruthy() + }) + }) + + describe('job is a cron', () => { + props.jobType = ServiceTypeEnum.CRON_JOB + + it('should render 5 input and 1 textarea', async () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues, + }) + ) + + expect(getAllByTestId(baseElement, 'input-text')).toHaveLength(4) + getByTestId(baseElement, 'input-text-image-entry-point') + getByTestId(baseElement, 'input-textarea-cmd-arguments') + + expect(baseElement).toBeTruthy() + }) + + it('should display the cron value in a human readable way', async () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues, + }) + ) + const inputSchedule = getByLabelText(baseElement, 'Schedule - Cron expression') + + await act(async () => { + fireEvent.change(inputSchedule, { target: { value: '9 * * * *' } }) + }) + + getByText(baseElement, 'At 9 minutes past the hour') + + expect(baseElement).toBeTruthy() + }) + }) +}) diff --git a/libs/shared/console-shared/src/lib/job-configure-settings/ui/job-configure-settings.tsx b/libs/shared/console-shared/src/lib/job-configure-settings/ui/job-configure-settings.tsx new file mode 100644 index 0000000000..467f184194 --- /dev/null +++ b/libs/shared/console-shared/src/lib/job-configure-settings/ui/job-configure-settings.tsx @@ -0,0 +1,207 @@ +import cronstrue from 'cronstrue' +import { useEffect, useState } from 'react' +import { Controller, useFormContext } from 'react-hook-form' +import { JobType, ServiceTypeEnum } from '@qovery/shared/enums' +import { JobConfigureData } from '@qovery/shared/interfaces' +import { EnableBox, InputText, Link, LoaderSpinner } from '@qovery/shared/ui' +import EntrypointCmdInputs from '../../entrypoint-cmd-inputs/ui/entrypoint-cmd-inputs' + +export interface JobConfigureSettingsProps { + jobType: JobType + loading?: boolean +} + +export function JobConfigureSettings(props: JobConfigureSettingsProps) { + const { loading } = props + const { control, watch } = useFormContext() + + const watchSchedule = watch('schedule') + const [cronDescription, setCronDescription] = useState('') + + useEffect(() => { + if (watchSchedule) { + // check if watchSchedule is a valid cron expression + const isValidCron = cronstrue.toString(watchSchedule, { throwExceptionOnParseError: false }) + if (isValidCron.indexOf('An error') === -1) { + setCronDescription(isValidCron) + } + } + }, [watchSchedule]) + + return loading ? ( + + ) : ( +
+ {props.jobType === ServiceTypeEnum.CRON_JOB ? ( + <> +

CRON

+ ( + + )} + /> +
+

{cronDescription}

+ +
+ + + ) : ( +
+

Event

+

+ Select one or more environment event where the job should be executed +

+ + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> +
+ )} + +

Parameters

+ ( + + )} + /> +

+ Maximum number of restarts allowed in case of job failure (0 means no failure) +

+ + ( + + )} + /> + +

+ Maximum duration allowed for the job to run before killing it and mark it as failed +

+ + ( + + )} + /> + +

+ Port where to run readiness and liveliness probes checks. The port will not be exposed externally +

+
+ ) +} + +export default JobConfigureSettings diff --git a/libs/shared/console-shared/src/lib/job-general-settings/ui/job-general-settings.spec.tsx b/libs/shared/console-shared/src/lib/job-general-settings/ui/job-general-settings.spec.tsx new file mode 100644 index 0000000000..6412c68ade --- /dev/null +++ b/libs/shared/console-shared/src/lib/job-general-settings/ui/job-general-settings.spec.tsx @@ -0,0 +1,94 @@ +import { getAllByTestId, getByTestId } from '@testing-library/react' +import { render } from '__tests__/utils/setup-jest' +import { wrapWithReactHookForm } from '__tests__/utils/wrap-with-react-hook-form' +import { BuildModeEnum, GitProviderEnum } from 'qovery-typescript-axios' +import { cronjobFactoryMock } from '@qovery/domains/application' +import { ServiceTypeEnum } from '@qovery/shared/enums' +import { JobGeneralData } from '@qovery/shared/interfaces' +import JobGeneralSettings from './job-general-settings' + +const mockJobApplication = cronjobFactoryMock(1)[0] +jest.mock('@qovery/domains/application', () => { + return { + ...jest.requireActual('@qovery/domains/application'), + editApplication: jest.fn(), + getApplicationsState: () => ({ + loadingStatus: 'loaded', + ids: [mockJobApplication.id], + entities: { + [mockJobApplication.id]: mockJobApplication, + }, + error: null, + }), + selectApplicationById: () => mockJobApplication, + } +}) + +describe('JobGeneralSettings', () => { + let defaultValues: JobGeneralData + + beforeEach(() => { + defaultValues = { + branch: 'main', + name: 'test', + description: 'test', + build_mode: BuildModeEnum.DOCKER, + dockerfile_path: 'Dockerfile', + root_path: '/', + image_entry_point: 'test', + repository: 'test', + provider: GitProviderEnum.GITHUB, + serviceType: ServiceTypeEnum.APPLICATION, + } + }) + + it('should render successfully', () => { + const { baseElement } = render( + wrapWithReactHookForm(, { + defaultValues, + }) + ) + expect(baseElement).toBeTruthy() + }) + + it('should render 2 content block if edit mode', () => { + const { baseElement } = render( + wrapWithReactHookForm( + , + { + defaultValues, + } + ) + ) + + expect(getAllByTestId(baseElement, 'block-content')).toHaveLength(2) + }) + + it('should render git related fields if service type is git', () => { + defaultValues.serviceType = ServiceTypeEnum.APPLICATION + const { baseElement } = render( + wrapWithReactHookForm( + , + { + defaultValues, + } + ) + ) + + getByTestId(baseElement, 'git-fields') + }) + + it('should render container related fields if service type is git', () => { + defaultValues.serviceType = ServiceTypeEnum.CONTAINER + const { baseElement } = render( + wrapWithReactHookForm( + , + { + defaultValues, + } + ) + ) + + getByTestId(baseElement, 'container-fields') + }) +}) diff --git a/libs/shared/console-shared/src/lib/job-general-settings/ui/job-general-settings.tsx b/libs/shared/console-shared/src/lib/job-general-settings/ui/job-general-settings.tsx new file mode 100644 index 0000000000..6914427b74 --- /dev/null +++ b/libs/shared/console-shared/src/lib/job-general-settings/ui/job-general-settings.tsx @@ -0,0 +1,126 @@ +import { BuildModeEnum } from 'qovery-typescript-axios' +import { Controller, useFormContext } from 'react-hook-form' +import { IconEnum, JobType, ServiceTypeEnum, isApplication, isContainer } from '@qovery/shared/enums' +import { JobGeneralData, OrganizationEntity } from '@qovery/shared/interfaces' +import { BlockContent, Icon, InputSelect, InputText } from '@qovery/shared/ui' +import CreateGeneralGitApplication from '../../create-general-git-application/ui/create-general-git-application' +import GeneralContainerSettings from '../../general-container-settings/ui/general-container-settings' +import EditGitRepositorySettingsFeature from '../../git-repository-settings/edit-git-repository-settings-feature/edit-git-repository-settings-feature' + +export interface JobGeneralSettingProps { + organization?: OrganizationEntity + jobType: JobType + isEdition?: boolean +} + +export function JobGeneralSettings(props: JobGeneralSettingProps) { + const { control, watch } = useFormContext() + const watchServiceType = watch('serviceType') + + return ( + <> + {!props.isEdition && ( + ( + , + }, + { + value: ServiceTypeEnum.CONTAINER, + label: 'Container Registry', + icon: , + }, + ]} + label="Application source" + error={error?.message} + /> + )} + /> + )} + + {watchServiceType && ( + <> + {isApplication(watchServiceType) && + (props.isEdition ? ( +
+ + + ( + + )} + /> + + ( + + )} + /> + +
+ ) : ( + + ))} + + {isContainer(watchServiceType) && + (props.isEdition ? ( +
+ + + +
+ ) : ( +
+ +
+ ))} + + )} + + ) +} + +export default JobGeneralSettings diff --git a/libs/shared/console-shared/src/lib/setting-resources/ui/setting-resources.tsx b/libs/shared/console-shared/src/lib/setting-resources/ui/setting-resources.tsx index e6c1c661d8..0a99cb4f65 100644 --- a/libs/shared/console-shared/src/lib/setting-resources/ui/setting-resources.tsx +++ b/libs/shared/console-shared/src/lib/setting-resources/ui/setting-resources.tsx @@ -1,4 +1,5 @@ import { Controller, useFormContext } from 'react-hook-form' +import { isJob } from '@qovery/shared/enums' import { ApplicationEntity } from '@qovery/shared/interfaces' import { BlockContent, @@ -89,7 +90,7 @@ export function SettingResources(props: SettingResourcesProps) { /> - {watchInstances && ( + {!isJob(application) && watchInstances && (

{`${watchInstances[0]} - ${watchInstances[1]}`}

`/organization/${organizationId}` diff --git a/libs/shared/router/src/lib/sub-router/application.router.ts b/libs/shared/router/src/lib/sub-router/application.router.ts index 266c39a0be..d5250dc8c9 100644 --- a/libs/shared/router/src/lib/sub-router/application.router.ts +++ b/libs/shared/router/src/lib/sub-router/application.router.ts @@ -12,6 +12,7 @@ export const APPLICATION_SETTINGS_URL = '/settings' export const APPLICATION_SETTINGS_GENERAL_URL = '/general' export const APPLICATION_SETTINGS_RESOURCES_URL = '/resources' +export const APPLICATION_SETTINGS_CONFIGURE_URL = '/configure' export const APPLICATION_SETTINGS_STORAGE_URL = '/storage' export const APPLICATION_SETTINGS_DOMAIN_URL = '/domain' export const APPLICATION_SETTINGS_PORT_URL = '/port' diff --git a/libs/shared/router/src/lib/sub-router/job.router.ts b/libs/shared/router/src/lib/sub-router/job.router.ts new file mode 100644 index 0000000000..974977bc7c --- /dev/null +++ b/libs/shared/router/src/lib/sub-router/job.router.ts @@ -0,0 +1,10 @@ +export const SERVICES_CRONJOB_CREATION_URL = '/create/cron-job' +export const SERVICES_LIFECYCLE_CREATION_URL = '/create/lifecyle-job' + +// subrouter for job steps /create/general /create/settings etc... +export const SERVICES_JOB_CREATION_GENERAL_URL = '/general' +export const SERVICES_JOB_CREATION_CONFIGURE_URL = '/configure' +export const SERVICES_JOB_CREATION_RESOURCES_URL = '/resources' +export const SERVICES_JOB_CREATION_POST_URL = '/post' +export const SERVICES_JOB_CREATION_PORT_URL = '/port' +export const SERVICES_JOB_CREATION_VARIABLE_URL = '/variable' diff --git a/libs/shared/ui/src/index.ts b/libs/shared/ui/src/index.ts index eb8a386348..101bcef8e4 100644 --- a/libs/shared/ui/src/index.ts +++ b/libs/shared/ui/src/index.ts @@ -79,3 +79,4 @@ export * from './lib/components/help-section/help-section' export * from './lib/components/truncate/truncate' export * from './lib/components/inputs/input-select/input-select' export * from './lib/components/table/table-row-deployment/table-row-deployment' +export * from './lib/components/enable-box/enable-box' diff --git a/libs/shared/ui/src/lib/components/block-content/block-content.tsx b/libs/shared/ui/src/lib/components/block-content/block-content.tsx index fcddf48cba..fa9e03e176 100644 --- a/libs/shared/ui/src/lib/components/block-content/block-content.tsx +++ b/libs/shared/ui/src/lib/components/block-content/block-content.tsx @@ -4,13 +4,22 @@ export interface BlockContentProps { customWidth?: string className?: string classNameContent?: string + dataTestId?: string } export function BlockContent(props: BlockContentProps) { - const { children, className = '', title, customWidth = 'w-full', classNameContent = 'p-5' } = props + const { + children, + className = '', + title, + customWidth = 'w-full', + classNameContent = 'p-5', + dataTestId = 'block-content', + } = props return (
diff --git a/libs/shared/ui/src/lib/components/enable-box/enable-box.spec.tsx b/libs/shared/ui/src/lib/components/enable-box/enable-box.spec.tsx new file mode 100644 index 0000000000..2fbe51d1dc --- /dev/null +++ b/libs/shared/ui/src/lib/components/enable-box/enable-box.spec.tsx @@ -0,0 +1,68 @@ +import { act, getByTestId, getByText, queryByText } from '@testing-library/react' +import { render } from '__tests__/utils/setup-jest' +import EnableBox, { EnableBoxProps } from './enable-box' + +const props: EnableBoxProps = { + checked: false, + setChecked: jest.fn(), + name: 'test', + description: 'test', + title: 'test', + children:
children content
, +} + +describe('EnableBox', () => { + it('should render successfully', () => { + const { baseElement } = render() + expect(baseElement).toBeTruthy() + }) + + it('should check the box if we click anywhere on the card when unchecked', async () => { + const { baseElement } = render() + const box = getByTestId(baseElement, 'enabled-box') + await act(() => { + box.click() + }) + + expect(props.setChecked).toHaveBeenCalledWith(true) + }) + + it('should not check the box if we click anywhere on the card when already checked', async () => { + const { baseElement } = render() + const box = getByTestId(baseElement, 'enabled-box') + await act(() => {}) + + await act(() => { + box.click() + }) + + expect(props.setChecked).not.toHaveBeenCalledWith(false) + }) + + it('should uncheck when you click on the checkbox', async () => { + const { baseElement } = render() + const box = getByTestId(baseElement, 'enabled-box') + const checkbox = getByTestId(box, 'input-checkbox') + + await act(() => { + checkbox.click() + }) + + expect(props.setChecked).toHaveBeenCalledWith(false) + }) + + it('should display children only when checked', async () => { + const { baseElement } = render() + const box = getByTestId(baseElement, 'enabled-box') + + const children = queryByText(baseElement, 'children content') + + expect(children).not.toBeTruthy() + + await act(() => { + box.click() + }) + + getByText(baseElement, 'children content') + }) +}) diff --git a/libs/shared/ui/src/lib/components/enable-box/enable-box.stories.tsx b/libs/shared/ui/src/lib/components/enable-box/enable-box.stories.tsx new file mode 100644 index 0000000000..800eda18ea --- /dev/null +++ b/libs/shared/ui/src/lib/components/enable-box/enable-box.stories.tsx @@ -0,0 +1,31 @@ +import { Meta, Story } from '@storybook/react' +import { EnableBox, EnableBoxProps } from './enable-box' + +export default { + component: EnableBox, + title: 'EnableBox', +} as Meta + +const Template: Story = (args) => ( + +
    +
  • Any
  • +
  • Dom
  • +
  • You
  • +
  • Like
  • +
+
+) + +export const Primary = Template.bind({}) +Primary.args = { + name: 'The box', + description: 'The box description', + title: 'The box title', + className: '', + dataTestId: '', + setChecked: () => { + console.log('checked') + }, + checked: false, +} diff --git a/libs/shared/ui/src/lib/components/enable-box/enable-box.tsx b/libs/shared/ui/src/lib/components/enable-box/enable-box.tsx new file mode 100644 index 0000000000..a93cbb761d --- /dev/null +++ b/libs/shared/ui/src/lib/components/enable-box/enable-box.tsx @@ -0,0 +1,65 @@ +import { FormEvent, ReactNode, useEffect, useState } from 'react' +import InputCheckbox from '../inputs/input-checkbox/input-checkbox' + +export interface EnableBoxProps { + checked: boolean | undefined + children: ReactNode + setChecked: (checked: boolean | undefined) => void + title: string + description: string + className?: string + name?: string + dataTestId?: string +} + +export function EnableBox(props: EnableBoxProps) { + const { + checked, + children, + setChecked, + title, + description, + className = '', + name = 'checkbox', + dataTestId = 'enabled-box', + } = props + + const [currentChecked, setCurrentChecked] = useState(checked) + + useEffect(() => { + if (checked !== undefined) setCurrentChecked(checked) + }, [checked]) + + useEffect(() => { + if (currentChecked !== checked) setChecked(currentChecked) + }, [currentChecked, setChecked, checked]) + + const checkedClasses = currentChecked + ? 'bg-brand-50 border border-brand-500' + : ' bg-element-light-lighter-200 border-element-light-lighter-500' + + return ( +
{ + if (!currentChecked) setCurrentChecked(!currentChecked) + }} + > + setCurrentChecked((e as FormEvent).currentTarget.checked)} + name={name} + label={title} + value={name} + isChecked={currentChecked} + big + /> + {description &&

{description}

} + + {currentChecked && children} +
+ ) +} + +export default EnableBox diff --git a/libs/shared/ui/src/lib/components/funnel-flow-body/funnel-flow-body.tsx b/libs/shared/ui/src/lib/components/funnel-flow-body/funnel-flow-body.tsx index 89e60824c2..9c44492dc3 100644 --- a/libs/shared/ui/src/lib/components/funnel-flow-body/funnel-flow-body.tsx +++ b/libs/shared/ui/src/lib/components/funnel-flow-body/funnel-flow-body.tsx @@ -12,7 +12,7 @@ export function FunnelFlowBody(props: FunnelFlowBodyProps) {
)}
-
+
= () => ( + + diff --git a/libs/shared/ui/src/lib/components/icon/icon.tsx b/libs/shared/ui/src/lib/components/icon/icon.tsx index 991dbf76ee..331ba52c69 100644 --- a/libs/shared/ui/src/lib/components/icon/icon.tsx +++ b/libs/shared/ui/src/lib/components/icon/icon.tsx @@ -16,6 +16,7 @@ import BuildpacksIcon from './icons/buildpacks' import ChildrenArrow from './icons/children-arrow' import { ContainerIcon } from './icons/container' import CronJobIcon from './icons/cron-job' +import CronJobStrokeIcon from './icons/cron-job-stroke-icon' import DatabaseIcon from './icons/database' import DOIcon from './icons/do' import DOGrayIcon from './icons/do-gray' @@ -24,6 +25,7 @@ import EnvironmentIcon from './icons/environment' import GitIcon from './icons/git' import InformationIcon from './icons/information' import LifecycleJobIcon from './icons/lifecycle-job' +import LifecycleJobStrokeIcon from './icons/lifecycle-job-stroke-icon' import MongoDBIcon from './icons/mongodb' import MysqlIcon from './icons/mysql' import PostgresqlIcon from './icons/postgresql' @@ -106,8 +108,12 @@ export function Icon(props: IconProps) { return case IconEnum.CRON_JOB: return + case IconEnum.CRON_JOB_STROKE: + return case IconEnum.LIFECYCLE_JOB: return + case IconEnum.LIFECYCLE_JOB_STROKE: + return default: return } diff --git a/libs/shared/ui/src/lib/components/icon/icons/cron-job-stroke-icon.tsx b/libs/shared/ui/src/lib/components/icon/icons/cron-job-stroke-icon.tsx new file mode 100644 index 0000000000..40e6def987 --- /dev/null +++ b/libs/shared/ui/src/lib/components/icon/icons/cron-job-stroke-icon.tsx @@ -0,0 +1,17 @@ +import { IconProps } from '../icon' + +export function CronJobStrokeIcon(props: IconProps) { + return ( + + + + + ) +} + +export default CronJobStrokeIcon diff --git a/libs/shared/ui/src/lib/components/icon/icons/lifecycle-job-stroke-icon.tsx b/libs/shared/ui/src/lib/components/icon/icons/lifecycle-job-stroke-icon.tsx new file mode 100644 index 0000000000..598ff0ca5c --- /dev/null +++ b/libs/shared/ui/src/lib/components/icon/icons/lifecycle-job-stroke-icon.tsx @@ -0,0 +1,44 @@ +import { IconProps } from '../icon' + +export function LifecycleJobStrokeIcon(props: IconProps) { + return ( + + + + + + + + + + + + + + ) +} + +export default LifecycleJobStrokeIcon diff --git a/libs/shared/ui/src/lib/components/inputs/input-checkbox/input-checkbox.tsx b/libs/shared/ui/src/lib/components/inputs/input-checkbox/input-checkbox.tsx index f2049cbb64..4576452b6f 100644 --- a/libs/shared/ui/src/lib/components/inputs/input-checkbox/input-checkbox.tsx +++ b/libs/shared/ui/src/lib/components/inputs/input-checkbox/input-checkbox.tsx @@ -12,6 +12,7 @@ export interface InputCheckboxProps { type?: string formValue?: string dataTestId?: string + big?: boolean } export function InputCheckbox(props: InputCheckboxProps) { @@ -27,16 +28,18 @@ export function InputCheckbox(props: InputCheckboxProps) { formValue, id = name, dataTestId = 'input-checkbox', + big = false, } = props const [check, setCheck] = useState(isChecked) + const bigClasses = big ? 'mr-6 before:w-5 before:h-5' : 'mr-5 before:w-4 before:h-4' useEffect(() => { setCheck(isChecked) }, [isChecked]) useEffect(() => { - setCheck(value === formValue) + if (formValue) setCheck(value === formValue) }, [formValue, value]) const inputChange = (check: boolean, e: FormEvent) => { @@ -55,14 +58,17 @@ export function InputCheckbox(props: InputCheckboxProps) { checked={check} disabled={disabled} onChange={(e) => inputChange(e.currentTarget.checked, e)} - className={`input-checkbox relative font-icons w-0 h-0 mr-5 appearance-none before:absolute before:flex before:justify-center before:items-center before:text-white before:w-4 before:h-4 before:top-0 before:left-0 before:-translate-y-1/2 before:rounded-sm before:bg-white ${ + className={`input-checkbox relative font-icons w-0 h-0 appearance-none before:absolute before:flex before:justify-center before:items-center before:text-white before:top-0 before:left-0 before:-translate-y-1/2 before:rounded-sm before:bg-white ${bigClasses} ${ disabled ? 'before:border-element-light-lighter-500' : 'before:border-element-light-lighter-700 cursor-pointer' } before:border-2 before:font-black before:text-xs before:leading-none before:content-[''] before:transition-all`} /> {label && ( -
diff --git a/libs/shared/ui/src/lib/styles/components/input.scss b/libs/shared/ui/src/lib/styles/components/input.scss index d310d27c35..cd9d6f4c3f 100644 --- a/libs/shared/ui/src/lib/styles/components/input.scss +++ b/libs/shared/ui/src/lib/styles/components/input.scss @@ -120,7 +120,7 @@ .input__select--small { @apply pl-2 pr-6 pt-2 pb-2 bg-element-light-lighter-200 border border-element-light-lighter-500 text-text-700 rounded text-sm appearance-none w-full min-h-0; - @apply focus:outline-brand-500 focus:border-0 focus:outline-2; + @apply focus:outline-brand-500 focus:border-transparent focus:outline-2; } // time diff --git a/libs/shared/utils/src/index.ts b/libs/shared/utils/src/index.ts index a4e2ab33d2..f896f1d20c 100644 --- a/libs/shared/utils/src/index.ts +++ b/libs/shared/utils/src/index.ts @@ -21,6 +21,7 @@ export * from './lib/tools/convert-cpu-to-vcpu' export * from './lib/tools/copy-to-clipboard' export * from './lib/tools/url-code-editor' export * from './lib/tools/build-git-repo-url' +export * from './lib/tools/compute-available-environment-variable-scope' // constants export * from './lib/constants/environment-mode-values' export * from './lib/constants/timezone-values' diff --git a/libs/pages/application/src/lib/utils/compute-available-environment-variable-scope.spec.tsx b/libs/shared/utils/src/lib/tools/compute-available-environment-variable-scope.spec.tsx similarity index 100% rename from libs/pages/application/src/lib/utils/compute-available-environment-variable-scope.spec.tsx rename to libs/shared/utils/src/lib/tools/compute-available-environment-variable-scope.spec.tsx diff --git a/libs/pages/application/src/lib/utils/compute-available-environment-variable-scope.tsx b/libs/shared/utils/src/lib/tools/compute-available-environment-variable-scope.tsx similarity index 100% rename from libs/pages/application/src/lib/utils/compute-available-environment-variable-scope.tsx rename to libs/shared/utils/src/lib/tools/compute-available-environment-variable-scope.tsx diff --git a/libs/shared/utils/src/lib/tools/refacto-payload.ts b/libs/shared/utils/src/lib/tools/refacto-payload.ts index 4c8beaaf3b..de654b424e 100644 --- a/libs/shared/utils/src/lib/tools/refacto-payload.ts +++ b/libs/shared/utils/src/lib/tools/refacto-payload.ts @@ -2,13 +2,19 @@ import { ApplicationEditRequest, ApplicationGitRepositoryRequest, DatabaseEditRequest, + JobRequest, Organization, OrganizationCustomRole, OrganizationCustomRoleUpdateRequest, OrganizationEditRequest, ServiceStorageStorage, } from 'qovery-typescript-axios' -import { ContainerApplicationEntity, DatabaseEntity, GitApplicationEntity } from '@qovery/shared/interfaces' +import { + ContainerApplicationEntity, + DatabaseEntity, + GitApplicationEntity, + JobApplicationEntity, +} from '@qovery/shared/interfaces' export function refactoPayload(response: any) { delete response['id'] @@ -82,6 +88,44 @@ export function refactoContainerApplicationPayload(application: Partial): JobRequest { + const jobRequest: JobRequest = { + name: job.name || '', + description: job.description || '', + cpu: job.cpu, + memory: job.memory, + auto_preview: false, + max_duration_seconds: job.max_duration_seconds, + port: job.port, + max_nb_restart: job.max_nb_restart, + } + + if (job.source?.docker) { + jobRequest.source = { + docker: { + dockerfile_path: job.source.docker.dockerfile_path, + git_repository: { + url: job.source.docker.git_repository?.url || '', + branch: job.source.docker.git_repository?.branch, + root_path: job.source.docker.git_repository?.root_path, + }, + }, + } + } else { + jobRequest.source = { + image: { + registry_id: job.source?.image?.registry_id, + image_name: job.source?.image?.image_name, + tag: job.source?.image?.tag, + }, + } + } + + jobRequest.schedule = job.schedule + + return jobRequest +} + export function refactoDatabasePayload(database: Partial) { const databaseRequestPayload: DatabaseEditRequest = { name: database.name, diff --git a/package.json b/package.json index c9e0ad1ee4..ed3a0a4a6d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "axios": "^0.27.2", "chance": "^1.1.8", "core-js": "^3.6.5", + "cronstrue": "^2.21.0", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.7", "fast-deep-equal": "^3.1.3", diff --git a/yarn.lock b/yarn.lock index 67614cbe2a..42b2c270e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1536,9 +1536,9 @@ integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== "@humanwhocodes/config-array@^0.11.6": - version "0.11.7" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" - integrity sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw== + version "0.11.8" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.8.tgz#03595ac2075a4dc0f191cc2131de14fbd7d410b9" + integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== dependencies: "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" @@ -5588,7 +5588,7 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.31": version "4.17.31" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== @@ -5598,12 +5598,12 @@ "@types/range-parser" "*" "@types/express@*", "@types/express@^4.17.13": - version "4.17.14" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" - integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== + version "4.17.15" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.15.tgz#9290e983ec8b054b65a5abccb610411953d417ff" + integrity sha512-Yv0k4bXGOH+8a+7bELd2PqHQsuiANB+A8a4gnQrkRWzrkKlb6KHaVvyXhqs04sVW/OWlbPyYxRgYlIXLfrufMQ== dependencies: "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" + "@types/express-serve-static-core" "^4.17.31" "@types/qs" "*" "@types/serve-static" "*" @@ -5786,9 +5786,9 @@ form-data "^3.0.0" "@types/node@*": - version "18.11.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.13.tgz#dff34f226ec1ac0432ae3b136ec5552bd3b9c0fe" - integrity sha512-IASpMGVcWpUsx5xBOrxMj7Bl8lqfuTY7FKAnPmu5cHkfQVWF8GulWS1jbRqA934qZL35xh5xN/+Xe/i26Bod4w== + version "18.11.15" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.15.tgz#de0e1fbd2b22b962d45971431e2ae696643d3f5d" + integrity sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw== "@types/node@18.7.18": version "18.7.18" @@ -5801,9 +5801,9 @@ integrity sha512-hcU9AIQVHmPnmjRK+XUUYlILlr9pQrsqSrwov/JK1pnf3GTQowVBhx54FbvM0AU/VXGH4i3+vgXS5EguR7fysA== "@types/node@^14.0.10 || ^16.0.0": - version "16.18.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.8.tgz#ffb2a4efa4eb4384811081776c52b054481cca54" - integrity sha512-TrpoNiaPvBH5h8rQQenMtVsJXtGsVBRJrcp2Ik6oEt99jHfGvDLh20VTTq3ixTbjYujukYz1IlY4N8a8yfY0jA== + version "16.18.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.9.tgz#47c491cfbc10460571d766c16526748fa9ad96a1" + integrity sha512-nhrqXYxiQ+5B/tPorWum37VgAiefi/wmfJ1QZKGKKecC8/3HqcTTJD0O+VABSPwtseMMF7NCPVT9uGgwn0YqsQ== "@types/normalize-package-data@^2.4.0": version "2.4.1" @@ -6151,13 +6151,13 @@ "@typescript-eslint/types" "5.43.0" "@typescript-eslint/visitor-keys" "5.43.0" -"@typescript-eslint/scope-manager@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.0.tgz#60790b14d0c687dd633b22b8121374764f76ce0d" - integrity sha512-7wWBq9d/GbPiIM6SqPK9tfynNxVbfpihoY5cSFMer19OYUA3l4powA2uv0AV2eAZV6KoAh6lkzxv4PoxOLh1oA== +"@typescript-eslint/scope-manager@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.46.1.tgz#70af8425c79bbc1178b5a63fb51102ddf48e104a" + integrity sha512-iOChVivo4jpwUdrJZyXSMrEIM/PvsbbDOX1y3UCKjSgWn+W89skxWaYXACQfxmIGhPVpRWK/VWPYc+bad6smIA== dependencies: - "@typescript-eslint/types" "5.46.0" - "@typescript-eslint/visitor-keys" "5.46.0" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/visitor-keys" "5.46.1" "@typescript-eslint/type-utils@5.41.0": version "5.41.0" @@ -6179,10 +6179,10 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.43.0.tgz#e4ddd7846fcbc074325293515fa98e844d8d2578" integrity sha512-jpsbcD0x6AUvV7tyOlyvon0aUsQpF8W+7TpJntfCUWU1qaIKu2K34pMwQKSzQH8ORgUrGYY6pVIh1Pi8TNeteg== -"@typescript-eslint/types@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.0.tgz#f4d76622a996b88153bbd829ea9ccb9f7a5d28bc" - integrity sha512-wHWgQHFB+qh6bu0IAPAJCdeCdI0wwzZnnWThlmHNY01XJ9Z97oKqKOzWYpR2I83QmshhQJl6LDM9TqMiMwJBTw== +"@typescript-eslint/types@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.46.1.tgz#4e9db2107b9a88441c4d5ecacde3bb7a5ebbd47e" + integrity sha512-Z5pvlCaZgU+93ryiYUwGwLl9AQVB/PQ1TsJ9NZ/gHzZjN7g9IAn6RSDkpCV8hqTwAiaj6fmCcKSQeBPlIpW28w== "@typescript-eslint/typescript-estree@5.41.0": version "5.41.0" @@ -6210,13 +6210,13 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/typescript-estree@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.0.tgz#a6c2b84b9351f78209a1d1f2d99ca553f7fa29a5" - integrity sha512-kDLNn/tQP+Yp8Ro2dUpyyVV0Ksn2rmpPpB0/3MO874RNmXtypMwSeazjEN/Q6CTp8D7ExXAAekPEcCEB/vtJkw== +"@typescript-eslint/typescript-estree@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.1.tgz#5358088f98a8f9939355e0996f9c8f41c25eced2" + integrity sha512-j9W4t67QiNp90kh5Nbr1w92wzt+toiIsaVPnEblB2Ih2U9fqBTyqV9T3pYWZBRt6QoMh/zVWP59EpuCjc4VRBg== dependencies: - "@typescript-eslint/types" "5.46.0" - "@typescript-eslint/visitor-keys" "5.46.0" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/visitor-keys" "5.46.1" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -6238,15 +6238,15 @@ semver "^7.3.7" "@typescript-eslint/utils@^5.36.1": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.0.tgz#600cd873ba471b7d8b0b9f35de34cf852c6fcb31" - integrity sha512-4O+Ps1CRDw+D+R40JYh5GlKLQERXRKW5yIQoNDpmXPJ+C7kaPF9R7GWl+PxGgXjB3PQCqsaaZUpZ9dG4U6DO7g== + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.46.1.tgz#7da3c934d9fd0eb4002a6bb3429f33298b469b4a" + integrity sha512-RBdBAGv3oEpFojaCYT4Ghn4775pdjvwfDOfQ2P6qzNVgQOVrnSPe5/Pb88kv7xzYQjoio0eKHKB9GJ16ieSxvA== dependencies: "@types/json-schema" "^7.0.9" "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.46.0" - "@typescript-eslint/types" "5.46.0" - "@typescript-eslint/typescript-estree" "5.46.0" + "@typescript-eslint/scope-manager" "5.46.1" + "@typescript-eslint/types" "5.46.1" + "@typescript-eslint/typescript-estree" "5.46.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" semver "^7.3.7" @@ -6267,12 +6267,12 @@ "@typescript-eslint/types" "5.43.0" eslint-visitor-keys "^3.3.0" -"@typescript-eslint/visitor-keys@5.46.0": - version "5.46.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.0.tgz#36d87248ae20c61ef72404bcd61f14aa2563915f" - integrity sha512-E13gBoIXmaNhwjipuvQg1ByqSAu/GbEpP/qzFihugJ+MomtoJtFAJG/+2DRPByf57B863m0/q7Zt16V9ohhANw== +"@typescript-eslint/visitor-keys@5.46.1": + version "5.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.1.tgz#126cc6fe3c0f83608b2b125c5d9daced61394242" + integrity sha512-jczZ9noovXwy59KjRTk1OftT78pwygdcmCuBf8yMoWt/8O8l+6x2LSEze0E4TeepXK4MezW3zGSyoDRZK7Y9cg== dependencies: - "@typescript-eslint/types" "5.46.0" + "@typescript-eslint/types" "5.46.1" eslint-visitor-keys "^3.3.0" "@vue/compiler-core@3.2.45": @@ -7292,9 +7292,9 @@ aws4@^1.8.0: integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== axe-core@^4.4.3: - version "4.5.2" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.5.2.tgz#823fdf491ff717ac3c58a52631d4206930c1d9f7" - integrity sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA== + version "4.6.1" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.1.tgz#79cccdee3e3ab61a8f42c458d4123a6768e6fbce" + integrity sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w== axios@0.27.2, axios@^0.21.2, axios@^0.21.4, axios@^0.27.2, axios@^1.0.0: version "0.27.2" @@ -8889,6 +8889,11 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cronstrue@^2.21.0: + version "2.21.0" + resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-2.21.0.tgz#278d19aa0b9e7ecc90a0c1dbd4f84ceece724094" + integrity sha512-YxabE1ZSHA1zJZMPCTSEbc0u4cRRenjqqTgCwJT7OvkspPSvfYFITuPFtsT+VkBuavJtFv2kJXT+mKSnlUJxfg== + cross-spawn@^6.0.0: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -8984,12 +8989,12 @@ css-loader@^5.0.1: semver "^7.3.5" css-loader@^6.4.0: - version "6.7.2" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.2.tgz#26bc22401b5921686a10fbeba75d124228302304" - integrity sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q== + version "6.7.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-6.7.3.tgz#1e8799f3ccc5874fdd55461af51137fcc5befbcd" + integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== dependencies: icss-utils "^5.1.0" - postcss "^8.4.18" + postcss "^8.4.19" postcss-modules-extract-imports "^3.0.0" postcss-modules-local-by-default "^4.0.0" postcss-modules-scope "^3.0.0" @@ -12085,11 +12090,11 @@ inquirer@^8.2.0: wrap-ansi "^7.0.0" internal-slot@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" - integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + version "1.0.4" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.4.tgz#8551e7baf74a7a6ba5f749cfb16aa60722f0d6f3" + integrity sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ== dependencies: - get-intrinsic "^1.1.0" + get-intrinsic "^1.1.3" has "^1.0.3" side-channel "^1.0.4" @@ -13573,9 +13578,9 @@ language-subtag-registry@^0.3.20: integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== language-tags@^1.0.5: - version "1.0.6" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.6.tgz#c087cc42cd92eb71f0925e9e271d4f8be5a93430" - integrity sha512-HNkaCgM8wZgE/BZACeotAAgpL9FUjEnhgF0FVQMIgH//zqTPreLYMb3rWYkYAqPoF75Jwuycp1da7uz66cfFQg== + version "1.0.7" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.7.tgz#41cc248730f3f12a452c2e2efe32bc0bbce67967" + integrity sha512-bSytju1/657hFjgUzPAPqszxH62ouE8nQFoFaVlIQfne4wO/wXC9A4+m8jYve7YBBvi59eq0SUpcshvG8h5Usw== dependencies: language-subtag-registry "^0.3.20" @@ -14858,9 +14863,9 @@ node-machine-id@^1.1.12: integrity sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ== node-releases@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" - integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + version "2.0.7" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.7.tgz#593edbc7c22860ee4d32d3933cfebdfab0c0e0e5" + integrity sha512-EJ3rzxL9pTWPjk5arA0s0dgXpnyiAbJDE6wHT62g7VsgrgQgmmZ+Ru++M1BFofncWja+Pnn3rEr3fieRySAdKQ== nopt@^6.0.0: version "6.0.0" @@ -16374,7 +16379,7 @@ postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.32, postcss@^7.0.36, postcss@^7.0 picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.1.10, postcss@^8.2.15, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.18: +postcss@^8.1.10, postcss@^8.2.15, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4.19: version "8.4.20" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.20.tgz#64c52f509644cecad8567e949f4081d98349dc56" integrity sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g== @@ -16384,9 +16389,9 @@ postcss@^8.1.10, postcss@^8.2.15, postcss@^8.3.5, postcss@^8.4.14, postcss@^8.4. source-map-js "^1.0.2" posthog-js@^1.24.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.37.0.tgz#906a64fb336e5a5d246a914ccc998eb6a1873254" - integrity sha512-yOyae9EBWM7FDeWCw5aGkxoM7GEzWEecK7r0XCA2zNoi01O6Txla79j/C9JLXySRiSR2J+dAzlFlaDKR6VMchg== + version "1.38.0" + resolved "https://registry.yarnpkg.com/posthog-js/-/posthog-js-1.38.0.tgz#51231f0d350ef2630f44dacb39bcb49a834143f3" + integrity sha512-d5IW992X8ccwTTVaaG28MNwJ2Ro/L8vDireK1UIzboyI19llzFkaN2mK9sZxlJ+E61cskjiGWDIN/NrSlbc94A== dependencies: "@sentry/types" "7.22.0" fflate "^0.4.1"