diff --git a/libs/domains/services/data-access/src/lib/domains-services-data-access.ts b/libs/domains/services/data-access/src/lib/domains-services-data-access.ts index afc5f92374e..e5250273db0 100644 --- a/libs/domains/services/data-access/src/lib/domains-services-data-access.ts +++ b/libs/domains/services/data-access/src/lib/domains-services-data-access.ts @@ -716,6 +716,7 @@ type DeployRequest = | { serviceId: string serviceType: DatabaseType + applyImmediately?: boolean } | { serviceId: string @@ -1015,8 +1016,8 @@ export const mutations = { mutation: containerActionsApi.deployContainer.bind(containerActionsApi, serviceId, request), serviceType, })) - .with({ serviceType: 'DATABASE' }, ({ serviceId, serviceType }) => ({ - mutation: databaseActionsApi.deployDatabase.bind(databaseActionsApi, serviceId), + .with({ serviceType: 'DATABASE' }, ({ serviceId, serviceType, applyImmediately = false }) => ({ + mutation: databaseActionsApi.deployDatabase.bind(databaseActionsApi, serviceId, applyImmediately), serviceType, })) .with({ serviceType: 'JOB' }, ({ serviceId, serviceType, forceEvent, request }) => ({ diff --git a/libs/domains/services/feature/src/index.ts b/libs/domains/services/feature/src/index.ts index 84ea1cbd87f..9ee5b72bb7b 100644 --- a/libs/domains/services/feature/src/index.ts +++ b/libs/domains/services/feature/src/index.ts @@ -65,3 +65,4 @@ export * from './lib/service-access-modal/service-access-modal' export * from './lib/service-deployment-list/service-deployment-list' export * from './lib/pod-details/pod-details' export * from './lib/force-unlock-modal/force-unlock-modal' +export * from './lib/database-deploy-modal/database-deploy-modal' diff --git a/libs/domains/services/feature/src/lib/database-deploy-modal/database-deploy-modal.tsx b/libs/domains/services/feature/src/lib/database-deploy-modal/database-deploy-modal.tsx new file mode 100644 index 00000000000..a1a55c5b446 --- /dev/null +++ b/libs/domains/services/feature/src/lib/database-deploy-modal/database-deploy-modal.tsx @@ -0,0 +1,118 @@ +import { type IconName } from '@fortawesome/fontawesome-common-types' +import * as Dialog from '@radix-ui/react-dialog' +import { type PropsWithChildren, type ReactNode } from 'react' +import { Controller, useForm } from 'react-hook-form' +import { Button, Heading, Icon, RadioGroup, Section, useModal } from '@qovery/shared/ui' +import { twMerge } from '@qovery/shared/util-js' + +type DatabaseDeployModalData = { + action: string + name: string +} + +interface ActionItem { + id: string // Also used as the text the user has to type to confirm + title: string + callback: (data: DatabaseDeployModalData) => void + description?: ReactNode + icon?: IconName +} + +export interface DatabaseDeployModalProps extends PropsWithChildren { + title: string + description?: ReactNode + actions: ActionItem[] + name?: string + submitButtonText?: string +} + +export function DatabaseDeployModal({ + title, + description, + actions, + children, + submitButtonText, +}: DatabaseDeployModalProps) { + const { + handleSubmit, + control, + watch, + formState: { isValid }, + } = useForm({ + mode: 'onChange', + defaultValues: { + name: '', + action: actions[0]?.id, + }, + }) + const { closeModal } = useModal() + + const selectedActionId = watch('action') + const selectedAction = actions.find((action) => action.id === selectedActionId) + + const onSubmit = handleSubmit((data) => { + if (data) { + closeModal() + selectedAction?.callback?.(data) + } + }) + + return ( +
+ + + {title} + + + + {description} + + +
+
+
+ ( + + {actions.map((action) => ( + + ))} + + )} + /> +
+
+ + {children} + +
+ + +
+
+
+ ) +} + +export default DatabaseDeployModal diff --git a/libs/domains/services/feature/src/lib/need-redeploy-flag/need-redeploy-flag.tsx b/libs/domains/services/feature/src/lib/need-redeploy-flag/need-redeploy-flag.tsx index e03f6133c79..eda13091490 100644 --- a/libs/domains/services/feature/src/lib/need-redeploy-flag/need-redeploy-flag.tsx +++ b/libs/domains/services/feature/src/lib/need-redeploy-flag/need-redeploy-flag.tsx @@ -1,7 +1,8 @@ import { ServiceDeploymentStatusEnum } from 'qovery-typescript-axios' import { useNavigate, useParams } from 'react-router-dom' import { DEPLOYMENT_LOGS_VERSION_URL, ENVIRONMENT_LOGS_URL } from '@qovery/shared/routes' -import { Banner } from '@qovery/shared/ui' +import { Banner, useModal } from '@qovery/shared/ui' +import DatabaseDeployModal from '../database-deploy-modal/database-deploy-modal' import { useDeployService } from '../hooks/use-deploy-service/use-deploy-service' import { useDeploymentStatus } from '../hooks/use-deployment-status/use-deployment-status' import { useService } from '../hooks/use-service/use-service' @@ -9,8 +10,10 @@ import { useService } from '../hooks/use-service/use-service' export function NeedRedeployFlag() { const { organizationId = '', projectId = '', environmentId = '', applicationId = '', databaseId = '' } = useParams() const navigate = useNavigate() + const { openModal } = useModal() const { data: service } = useService({ environmentId, serviceId: applicationId || databaseId }) + const { data: serviceDeploymentStatus } = useDeploymentStatus({ environmentId, serviceId: service?.id, @@ -31,9 +34,9 @@ export function NeedRedeployFlag() { const buttonLabel = (serviceDeploymentStatusState === ServiceDeploymentStatusEnum.OUT_OF_DATE ? 'Redeploy' : 'Deploy') + ' now' - const mutationDeployService = () => { + const mutationDeployService = (applyImmediately = false) => { if (service) { - deployService({ serviceId: service.id, serviceType: service.serviceType }) + deployService({ serviceId: service.id, serviceType: service.serviceType, applyImmediately }) navigate( ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) + DEPLOYMENT_LOGS_VERSION_URL(service.id, 'latest') @@ -41,13 +44,73 @@ export function NeedRedeployFlag() { } } + const handleDatabaseDeployModal = () => { + openModal({ + content: ( + + Redeploy your database and apply changes during the next maintenance window. + + ), + icon: 'calendar-clock', + callback: () => { + try { + mutationDeployService(false) + } catch (error) { + console.error(error) + } + }, + }, + { + id: 'immediately', + title: 'Immediately', + description: ( +
+
+ Redeploy your database and apply changes immediately. +

+ Be careful, + your database may be unavailable for a few minutes during this process. +

+
+
+ ), + icon: 'timer', + callback: () => { + try { + mutationDeployService(true) + } catch (error) { + console.error(error) + } + }, + }, + ]} + submitButtonText="Confirm" + /> + ), + options: { + width: 740, + }, + }) + } + + const handleDeploy = () => { + if (service?.serviceType === 'DATABASE' && service.mode === 'MANAGED') { + handleDatabaseDeployModal() + } else { + mutationDeployService(false) + } + } + return ( - + {serviceDeploymentStatusState === ServiceDeploymentStatusEnum.NEVER_DEPLOYED ? (

This service is not running

) : ( diff --git a/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx b/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx index 16bdf74058f..e81bb895b30 100644 --- a/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx +++ b/libs/domains/services/feature/src/lib/service-action-toolbar/service-action-toolbar.tsx @@ -62,6 +62,7 @@ import { urlCodeEditor, } from '@qovery/shared/util-js' import { ConfirmationCancelLifecycleModal } from '../confirmation-cancel-lifecycle-modal/confirmation-cancel-lifecycle-modal' +import { DatabaseDeployModal } from '../database-deploy-modal/database-deploy-modal' import { ForceUnlockModal } from '../force-unlock-modal/force-unlock-modal' import { useCancelDeploymentService } from '../hooks/use-cancel-deployment-service/use-cancel-deployment-service' import { useDeleteService } from '../hooks/use-delete-service/use-delete-service' @@ -77,7 +78,7 @@ import { SelectCommitModal } from '../select-commit-modal/select-commit-modal' import { SelectVersionModal } from '../select-version-modal/select-version-modal' import { ServiceAvatar } from '../service-avatar/service-avatar' import { ServiceCloneModal } from '../service-clone-modal/service-clone-modal' -import useServiceRemoveModal from '../service-remove-modal/use-service-remove-modal/use-service-remove-modal' +import { useServiceRemoveModal } from '../service-remove-modal/use-service-remove-modal/use-service-remove-modal' type ActionToolbarVariant = 'default' | 'deployment' @@ -138,7 +139,10 @@ function MenuManageDeployment({ const tooltipServiceNeedUpdate = displayYellowColor && tooltipService('Configuration has changed and needs to be applied') - const mutationDeploy = () => deployService({ serviceId: service.id, serviceType: service.serviceType }) + const mutationDeploy = (applyImmediately = false) => { + deployService({ serviceId: service.id, serviceType: service.serviceType, applyImmediately }) + } + const mutationTerraformAction = ( action: 'plan' | 'plan_and_apply' | 'destroy' | 'force_unlock' | 'migrate_state' ) => { @@ -382,6 +386,71 @@ function MenuManageDeployment({ }) } + const handleDatabaseDeployModal = () => { + openModal({ + content: ( + + Redeploy your database and apply changes during the next maintenance window. + + ), + icon: 'calendar-clock', + callback: () => { + try { + mutationDeploy(false) + } catch (error) { + console.error(error) + } + }, + }, + { + id: 'immediately', + title: 'Immediately', + description: ( +
+
+ Redeploy your database and apply changes immediately. +

+ Be careful, + your database may be unavailable for a few minutes during this process. +

+
+
+ ), + icon: 'timer', + callback: () => { + try { + mutationDeploy(true) + } catch (error) { + console.error(error) + } + }, + }, + ]} + submitButtonText="Confirm" + /> + ), + options: { + width: 740, + }, + }) + } + + const handleDeploy = () => { + if (service.serviceType === 'DATABASE' && service.mode === 'MANAGED') { + handleDatabaseDeployModal() + } else { + mutationDeploy(false) + } + } + return ( @@ -461,7 +530,7 @@ function MenuManageDeployment({ {isDeployAvailable(state) && ( } - onSelect={mutationDeploy} + onSelect={handleDeploy} className="relative" color={displayYellowColor ? 'yellow' : 'brand'} >