Skip to content
Draft
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
1 change: 1 addition & 0 deletions libs/domains/environments/feature/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ export * from './lib/hooks/use-lifecycle-templates/use-lifecycle-templates'
export * from './lib/hooks/use-lifecycle-template/use-lifecycle-template'
export * from './lib/hooks/use-deploy-all-services/use-deploy-all-services'
export * from './lib/hooks/use-service-count/use-service-count'
export * from './lib/hooks/use-services-for-deploy/use-services-for-deploy'
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing

exports[`EnvironmentActionToolbar should match manage deployment snapshot 1`] = `
<body
Expand Down Expand Up @@ -186,9 +186,9 @@ exports[`EnvironmentActionToolbar should match manage deployment snapshot 1`] =
>
<i
aria-hidden="true"
class="fa-solid fa-rotate text-sm mr-3 text-brand-500"
class="fa-solid fa-code-branch text-sm mr-3 text-brand-500"
/>
Deploy latest version for..
Deploy by version
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ function MenuManageDeployment({
content: <UpdateAllModal environment={environment} />,
options: {
width: 676,
fakeModal: true,
},
})
}
Expand Down Expand Up @@ -234,8 +235,8 @@ function MenuManageDeployment({
.otherwise(() => (
<>
<DropdownMenu.Separator />
<DropdownMenu.Item icon={<Icon iconName="rotate" />} onSelect={openUpdateAllModal}>
Deploy latest version for..
<DropdownMenu.Item icon={<Icon iconName="code-branch" />} onSelect={openUpdateAllModal}>
Deploy by version
</DropdownMenu.Item>
</>
))}
Expand All @@ -245,7 +246,6 @@ function MenuManageDeployment({
}

function MenuOtherActions({ state, environment }: { state: StateEnum; environment: Environment }) {
const { pathname } = useLocation()
const { openModal, closeModal } = useModal()
const { openModalConfirmation } = useModalConfirmation()
const { mutate: deleteEnvironment } = useDeleteEnvironment({ projectId: environment.project.id })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { renderHook } from '@testing-library/react'
import { type PropsWithChildren } from 'react'
import { useServicesForDeploy } from './use-services-for-deploy'

// Mock the queries module
jest.mock('@qovery/state/util-queries', () => ({
queries: {
services: {
list: jest.fn(() => ({
queryKey: ['services', 'list', 'env-1'],
queryFn: jest.fn(),
})),
},
},
}))

describe('useServicesForDeploy', () => {
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: PropsWithChildren) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}

it('should return empty array when no environmentId', () => {
const { result } = renderHook(() => useServicesForDeploy({ environmentId: '' }), {
wrapper: createWrapper(),
})

expect(result.current.data).toEqual([])
})

it('should return isLoading state', () => {
const { result } = renderHook(() => useServicesForDeploy({ environmentId: 'env-1' }), {
wrapper: createWrapper(),
})

// Initially should be loading or have data
expect(typeof result.current.isLoading).toBe('boolean')
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import { useQuery } from '@tanstack/react-query'
import { type ContainerRegistryProviderDetailsResponse, type ContainerSource } from 'qovery-typescript-axios'
import { useMemo } from 'react'
import {
type AnyService,
type ServiceType,
isApplication,
isContainer,
isDatabase,
isHelm,
isJob,
} from '@qovery/domains/services/data-access'
import {
isHelmGitSource,
isHelmGitValuesOverride,
isHelmRepositorySource,
isJobContainerSource,
isJobGitSource,
} from '@qovery/shared/enums'
import { queries } from '@qovery/state/util-queries'

export type VersionSourceType =
| 'git' // Application, Job (docker), Helm (git), Terraform
| 'container' // Container, Job (image)
| 'helm-repository' // Helm (repository)
| 'database' // No version selection

export type VersionType = 'commit' | 'tag' | 'chart-version'

export interface ServiceVersionInfo {
type: VersionType
value: string
displayValue: string
}

export interface ServiceForDeploy {
id: string
name: string
icon_uri: string
serviceType: ServiceType
job_type?: 'CRON' | 'LIFECYCLE'
sourceType: VersionSourceType
currentVersion?: ServiceVersionInfo

// For containers/jobs with image source - registry info needed for version fetch
containerSource?: ContainerSource

// For Helm with repository source - chart info needed for version fetch
helmRepository?: {
repositoryId: string
chartName: string
}

// For git-based services - needed for commit fetch
gitRepository?: {
deployedCommitId?: string
deployedCommitDate?: string
}

// For Helm with git values override
hasValuesOverrideGit?: boolean
}

function mapServiceToDeployInfo(service: AnyService): ServiceForDeploy {
if (isApplication(service)) {
const gitRepo = service.git_repository
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'APPLICATION',
sourceType: 'git',
currentVersion: gitRepo?.deployed_commit_id
? {
type: 'commit',
value: gitRepo.deployed_commit_id,
displayValue: gitRepo.deployed_commit_id.slice(0, 7),
}
: undefined,
gitRepository: gitRepo
? {
deployedCommitId: gitRepo.deployed_commit_id,
deployedCommitDate: gitRepo.deployed_commit_date,
}
: undefined,
}
}

if (isContainer(service)) {
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'CONTAINER',
sourceType: 'container',
currentVersion: service.tag
? {
type: 'tag',
value: service.tag,
displayValue: service.tag,
}
: undefined,
containerSource: {
image_name: service.image_name,
tag: service.tag,
registry: service.registry as ContainerRegistryProviderDetailsResponse,
},
}
}

if (isJob(service)) {
const source = service.source
if (isJobGitSource(source)) {
const gitRepo = source.docker?.git_repository
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'JOB',
job_type: service.job_type,
sourceType: 'git',
currentVersion: gitRepo?.deployed_commit_id
? {
type: 'commit',
value: gitRepo.deployed_commit_id,
displayValue: gitRepo.deployed_commit_id.slice(0, 7),
}
: undefined,
gitRepository: gitRepo
? {
deployedCommitId: gitRepo.deployed_commit_id,
deployedCommitDate: gitRepo.deployed_commit_date,
}
: undefined,
}
}

if (isJobContainerSource(source)) {
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'JOB',
job_type: service.job_type,
sourceType: 'container',
currentVersion: source.image?.tag
? {
type: 'tag',
value: source.image.tag,
displayValue: source.image.tag,
}
: undefined,
containerSource: source.image
? {
image_name: source.image.image_name ?? '',
tag: source.image.tag,
registry: source.image.registry as ContainerRegistryProviderDetailsResponse,
}
: undefined,
}
}

// Fallback for unknown job source
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'JOB',
job_type: service.job_type,
sourceType: 'git',
}
}

if (isHelm(service)) {
const source = service.source

if (isHelmGitSource(source)) {
const gitRepo = source.git?.git_repository
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'HELM',
sourceType: 'git',
currentVersion: gitRepo?.deployed_commit_id
? {
type: 'commit',
value: gitRepo.deployed_commit_id,
displayValue: gitRepo.deployed_commit_id.slice(0, 7),
}
: undefined,
gitRepository: gitRepo
? {
deployedCommitId: gitRepo.deployed_commit_id,
deployedCommitDate: gitRepo.deployed_commit_date,
}
: undefined,
hasValuesOverrideGit: isHelmGitValuesOverride(service.values_override),
}
}

if (isHelmRepositorySource(source)) {
const repo = source.repository
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'HELM',
sourceType: 'helm-repository',
currentVersion: repo?.chart_version
? {
type: 'chart-version',
value: repo.chart_version,
displayValue: repo.chart_version,
}
: undefined,
helmRepository: repo?.repository?.id
? {
repositoryId: repo.repository.id,
chartName: repo.chart_name ?? '',
}
: undefined,
hasValuesOverrideGit: isHelmGitValuesOverride(service.values_override),
}
}

// Fallback for unknown helm source
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'HELM',
sourceType: 'helm-repository',
}
}

if (isDatabase(service)) {
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'DATABASE',
sourceType: 'database',
currentVersion: service.version
? {
type: 'tag',
value: service.version,
displayValue: service.version,
}
: undefined,
}
}

// Terraform
const terraform = service
const tfGitRepo = terraform.terraform_files_source?.git?.git_repository
return {
id: service.id,
name: service.name,
icon_uri: service.icon_uri,
serviceType: 'TERRAFORM',
sourceType: 'git',
currentVersion: tfGitRepo?.deployed_commit_id
? {
type: 'commit',
value: tfGitRepo.deployed_commit_id,
displayValue: tfGitRepo.deployed_commit_id.slice(0, 7),
}
: undefined,
gitRepository: tfGitRepo
? {
deployedCommitId: tfGitRepo.deployed_commit_id,
deployedCommitDate: tfGitRepo.deployed_commit_date,
}
: undefined,
}
}

export interface UseServicesForDeployProps {
environmentId: string
}

export function useServicesForDeploy({ environmentId }: UseServicesForDeployProps) {
const { data: services = [], isLoading } = useQuery({
...queries.services.list(environmentId),
enabled: Boolean(environmentId),
})

const servicesForDeploy = useMemo(() => services.map(mapServiceToDeployInfo), [services])

return {
data: servicesForDeploy,
isLoading,
}
}

export default useServicesForDeploy
Loading