Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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,108 @@
import { type PropsWithChildren, type ReactNode } from 'react'
import { Controller, useForm } from 'react-hook-form'
import { Button, Icon, RadioGroup, useModal } from '@qovery/shared/ui'
import { twMerge } from '@qovery/shared/util-js'
import { type ActionItem, type DatabaseDeployModalData } from './use-database-deploy-modal/use-database-deploy-modal'

export interface DatabaseDeployModalProps extends PropsWithChildren {
title: string
description?: ReactNode
actions: ActionItem[]
name?: string
entities?: ReactNode[]
submitButtonText?: string
}

export function DatabaseDeployModal({
title,
description,
actions,
children,
entities,
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 (
<div className="p-6">
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you use component Section + Heading here ? And to avoid having the warning Dialog without title could you add <Dialog.Title> as well?

<h2 className="h4 mb-1 max-w-sm text-neutral-400 dark:text-neutral-50">{title}</h2>
<div className="mb-4">
{description && <div className="text-sm text-neutral-350 dark:text-neutral-50">{description}</div>}
{entities && <div className="mt-1 flex gap-1.5">{entities.map((entity) => entity)}</div>}
</div>

<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={selectedAction?.color ? selectedAction?.color : 'brand'}
disabled={!isValid}
>
{submitButtonText ?? selectedAction?.title}
</Button>
</div>
</form>
</div>
)
}

export default DatabaseDeployModal
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { type IconName } from '@fortawesome/fontawesome-common-types'
import { type EnvironmentModeEnum } from 'qovery-typescript-axios'
import { type ReactNode, useEffect, useState } from 'react'
import { useModal } from '@qovery/shared/ui'
import { DatabaseDeployModal } from '../database-deploy-modal'

export type DatabaseDeployModalData = {
action: string
name: string
}

export 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
color?: 'brand' | 'red' | 'yellow' | 'green' | 'neutral'
}

export interface UseDatabaseDeployModalProps {
title: string
actions: ActionItem[]
entities?: ReactNode[]
description?: ReactNode
name?: string
submitButtonText?: string
mode?: keyof typeof EnvironmentModeEnum | string | undefined
warning?: ReactNode
}

export function useDatabaseDeployModal() {
Comment thread
rmnbrd marked this conversation as resolved.
Outdated
const [databaseDeployModal, openDatabaseDeployModal] = useState<UseDatabaseDeployModalProps>()
const { openModal } = useModal()

useEffect(() => {
if (databaseDeployModal) {
openModal({
content: (
<DatabaseDeployModal
title={databaseDeployModal.title}
actions={databaseDeployModal.actions}
entities={databaseDeployModal.entities}
description={databaseDeployModal.description}
name={databaseDeployModal.name}
submitButtonText={databaseDeployModal.submitButtonText}
/>
),
options: {
width: 740,
},
})
}
}, [databaseDeployModal, openModal])

return { openDatabaseDeployModal }
}

export default useDatabaseDeployModal
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
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 { useDatabaseDeployModal } from '../database-deploy-modal/use-database-deploy-modal/use-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 { closeModal } = useModal()
const { openDatabaseDeployModal } = useDatabaseDeployModal()

const { data: service } = useService({ environmentId, serviceId: applicationId || databaseId })

const { data: serviceDeploymentStatus } = useDeploymentStatus({
environmentId,
serviceId: service?.id,
Expand All @@ -31,23 +35,79 @@ 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 = () => {
openDatabaseDeployModal({
title: `Deploy database`,
description: 'Choose when to deploy and apply your changes',
entities: [],
submitButtonText: 'Confirm',
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',
color: 'brand',
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',
color: 'brand',
callback: () => {
try {
mutationDeployService(true)
} catch (error) {
console.error(error)
}
},
},
],
})
}

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
Loading
Loading