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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ type DeployRequest =
| {
serviceId: string
serviceType: DatabaseType
applyImmediately?: boolean
}
| {
serviceId: string
Expand Down Expand Up @@ -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 }) => ({
Expand Down
1 change: 1 addition & 0 deletions libs/domains/services/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Original file line number Diff line number Diff line change
@@ -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<DatabaseDeployModalData>({
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 (
<Section className="p-5">
<Dialog.Title asChild>
<Heading level={1} className="text-neutral mb-2 max-w-sm text-2xl">
{title}
</Heading>
</Dialog.Title>
<Dialog.Description className="mb-6 text-sm text-neutral-350 dark:text-neutral-50">
{description}
</Dialog.Description>

<form onSubmit={onSubmit} className="space-y-6">
<div className="flex flex-col gap-5">
<div className="flex w-full flex-col gap-4">
<Controller
name="action"
control={control}
render={({ field }) => (
<RadioGroup.Root onValueChange={field.onChange} value={field.value} className="grid grid-cols-2 gap-4">
{actions.map((action) => (
<label
key={action.id}
className={twMerge(
'flex cursor-pointer flex-col gap-2 rounded border border-neutral-250 bg-neutral-100 p-5 text-left text-sm shadow transition-all',
selectedActionId === action.id && 'border-brand-500 bg-brand-50'
)}
>
<div className="flex items-center gap-3">
<RadioGroup.Item value={action.id} />
{action.icon && (
<Icon iconName={action.icon} iconStyle="regular" className="text-base text-neutral-350" />
)}
<span className="font-medium text-neutral-400">{action.title}</span>
</div>
<div className="inline-block text-sm text-neutral-350">{action.description}</div>
</label>
))}
</RadioGroup.Root>
)}
/>
</div>
</div>

{children}

<div className="flex justify-end gap-3">
<Button type="button" color="neutral" variant="plain" size="lg" onClick={() => closeModal()}>
Cancel
</Button>
<Button type="submit" size="lg" color="brand" disabled={!isValid}>
{submitButtonText ?? selectedAction?.title}
</Button>
</div>
</form>
</Section>
)
}

export default DatabaseDeployModal
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
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'

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,
Expand All @@ -31,23 +34,83 @@ 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 })
Comment thread
rmnbrd marked this conversation as resolved.
navigate(
ENVIRONMENT_LOGS_URL(organizationId, projectId, environmentId) +
DEPLOYMENT_LOGS_VERSION_URL(service.id, 'latest')
)
}
}

const handleDatabaseDeployModal = () => {
openModal({
content: (
<DatabaseDeployModal
title="Deploy database"
description="Choose when to deploy and apply your changes"
actions={[
{
id: 'next',
title: 'Next maintenance window',
description: (
<div className="flex flex-col gap-2 text-neutral-350">
Redeploy your database and apply changes during the next maintenance window.
</div>
),
icon: 'calendar-clock',
callback: () => {
try {
mutationDeployService(false)
} catch (error) {
console.error(error)
}
},
},
{
id: 'immediately',
title: 'Immediately',
description: (
<div className="flex flex-col gap-2 text-neutral-350">
<div className="flex flex-col gap-1">
<span>Redeploy your database and apply changes immediately.</span>
<p>
<span className="font-bold">Be careful, </span>
<span>your database may be unavailable for a few minutes during this process.</span>
</p>
</div>
</div>
),
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 (
<Banner
color="yellow"
buttonIconRight="rotate-right"
buttonLabel={buttonLabel}
onClickButton={mutationDeployService}
>
<Banner color="yellow" buttonIconRight="rotate-right" buttonLabel={buttonLabel} onClickButton={handleDeploy}>
{serviceDeploymentStatusState === ServiceDeploymentStatusEnum.NEVER_DEPLOYED ? (
<p>This service is not running</p>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'

Expand Down Expand Up @@ -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 })
Comment thread
rmnbrd marked this conversation as resolved.
}

const mutationTerraformAction = (
action: 'plan' | 'plan_and_apply' | 'destroy' | 'force_unlock' | 'migrate_state'
) => {
Expand Down Expand Up @@ -382,6 +386,71 @@ function MenuManageDeployment({
})
}

const handleDatabaseDeployModal = () => {
openModal({
content: (
<DatabaseDeployModal
title="Deploy database"
description="Choose when to deploy and apply your changes"
actions={[
{
id: 'next',
title: 'Next maintenance window',
description: (
<div className="flex flex-col gap-2 text-neutral-350">
Redeploy your database and apply changes during the next maintenance window.
</div>
),
icon: 'calendar-clock',
callback: () => {
try {
mutationDeploy(false)
} catch (error) {
console.error(error)
}
},
},
{
id: 'immediately',
title: 'Immediately',
description: (
<div className="flex flex-col gap-2 text-neutral-350">
<div className="flex flex-col gap-1">
<span>Redeploy your database and apply changes immediately.</span>
<p>
<span className="font-bold">Be careful, </span>
<span>your database may be unavailable for a few minutes during this process.</span>
</p>
</div>
</div>
),
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 (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
Expand Down Expand Up @@ -461,7 +530,7 @@ function MenuManageDeployment({
{isDeployAvailable(state) && (
<DropdownMenu.Item
icon={<Icon iconName="play" />}
onSelect={mutationDeploy}
onSelect={handleDeploy}
className="relative"
color={displayYellowColor ? 'yellow' : 'brand'}
>
Expand Down
Loading