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
11 changes: 10 additions & 1 deletion apps/client/src/components/EnvironmentForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
CleanedCluster,
CreateEnvironment,
Environment,
EnvironmentWithDeploymentsCount,
} from '@cpn-console/shared'
import {
deleteValidationInput,
Expand All @@ -24,7 +25,7 @@ interface OptionType {
}

const props = withDefaults(defineProps<{
environment?: Partial<Omit<Environment, 'updatedAt' | 'createdAt'>>
environment?: Partial<Omit<EnvironmentWithDeploymentsCount, 'updatedAt' | 'createdAt'>>
isEditable?: boolean
canManage: boolean
isProjectLocked?: boolean
Expand Down Expand Up @@ -343,6 +344,14 @@ watch(localEnvironment.value, () => {
v-if="isDeletingEnvironment"
class="fr-mt-4w"
>
<DsfrAlert
v-if="localEnvironment.deploymentsCount"
data-testid="linkedDeploymentsAlert"
class="fr-mb-2w"
:description="`Cet environnement est lié à ${localEnvironment.deploymentsCount} déploiement${localEnvironment.deploymentsCount > 1 ? 's' : ''}. Leur suppression sera également effectuée et est irréversible.`"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

thought: Note pour plus tard: Mettre en place l'i18n ne serait-ce que pour gérer sainement la pluralisation 😂

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestion: "Leur suppression sera également effectuée et est irréversible." -> " Leur suppression sera également effectuée. Cette opération est irréversible."

type="warning"
small
/>
<DsfrInput
v-model="environmentToDelete"
data-testid="deleteEnvironmentInput"
Expand Down
8 changes: 4 additions & 4 deletions apps/client/src/components/ProjectResources.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { CleanedCluster, Cluster, CreateEnvironment, Environment, Repo, Stage, UpdateEnvironment, Zone } from '@cpn-console/shared'
import type { CleanedCluster, Cluster, CreateEnvironment, Environment, EnvironmentWithDeploymentsCount, Repo, Stage, UpdateEnvironment, Zone } from '@cpn-console/shared'
import type { Project } from '@/utils/project-utils.js'
import type { RepoFormResult } from '@/utils/repository-utils.js'
import { logger } from '@cpn-console/logger/browser'
Expand Down Expand Up @@ -27,7 +27,7 @@ const snackbarStore = useSnackbarStore()
const stageStore = useStageStore()
const userStore = useUserStore()

const environments = ref<(Environment & { cluster?: Cluster, zone?: Zone, stage?: Stage })[]>([])
const environments = ref<(EnvironmentWithDeploymentsCount & { cluster?: Cluster, zone?: Zone, stage?: Stage })[]>([])
const repositories = ref<(Repo & { source: Source })[]>([])
const projectUsage = ref<({
hprod: { cpu: number, gpu: number, memory: number }
Expand All @@ -45,7 +45,7 @@ const repositoriesId = 'repositoriesTable'
const environmentsId = 'environmentsTable'
const syncFormId = 'syncFormId'
const selectedRepo = ref<Repo>()
const selectedEnv = ref<Environment>()
const selectedEnv = ref<EnvironmentWithDeploymentsCount>()
const newResource = ref<'repo' | 'env'>()
const hideEnvs = computed(() => props.asProfile === 'user' && !ProjectAuthorized.ListEnvironments({ projectPermissions: props.project.myPerms }))
const hideRepos = computed(() => props.asProfile === 'user' && !ProjectAuthorized.ListRepositories({ projectPermissions: props.project.myPerms }))
Expand Down Expand Up @@ -139,7 +139,7 @@ const timeAgo = new TimeAgo('fr-FR')

async function reload() {
environments.value = await props.project.Environments.list()
.then(envs => envs.map((environment: Environment) => {
.then(envs => envs.map((environment: EnvironmentWithDeploymentsCount) => {
const cluster = clusterStore.clusters.find(cluster => cluster.id === environment.clusterId)
const zone = zoneStore.zones.find(zone => zone.id === cluster?.zoneId)
const stage = stageStore.stages.find(stage => stage.id === environment.stageId)
Expand Down
3 changes: 2 additions & 1 deletion apps/client/src/utils/project-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
CreateRepositoryBodyV2,
Deployment,
Environment,
EnvironmentWithDeploymentsCount,
GetLogsQuery,
PermissionTarget,
PluginsUpdateBody,
Expand Down Expand Up @@ -83,7 +84,7 @@ export class Project implements ProjectV2 {
operationsInProgress: Ref<ProjectOperations[]>
myPerms: bigint
repositories: Ref<Repo[]>
environments: Ref<Environment[]>
environments: Ref<EnvironmentWithDeploymentsCount[]>
deployments: Ref<Deployment[]>
services: ProjectService[] = []
lastSuccessProvisionningVersion: string | null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { beforeEach, describe, expect, it } from 'vitest'
import { mockDeep } from 'vitest-mock-extended'
import { PrismaService } from '../infrastructure/database/prisma.service'
import { EnvironmentDatastoreService } from './environment-datastore.service'
import { makeCluster, makeEnvironment, makeStage } from './environment-testing.utils'
import { makeCluster, makeEnvironment, makeEnvironmentWithStageAndCount, makeStage } from './environment-testing.utils'

describe('environmentDatastoreService', () => {
let module: TestingModule
Expand Down Expand Up @@ -41,15 +41,18 @@ describe('environmentDatastoreService', () => {
})

describe('getEnvironmentsByProjectId', () => {
it('should return environments for a project with their stage', async () => {
const environments = [makeEnvironment({ projectId: 'project1' })]
it('should return environments for a project with their stage and their deployments count', async () => {
const environments = [makeEnvironmentWithStageAndCount({ projectId: 'project1', _count: { deployments: 2 } })]
prisma.environment.findMany.mockResolvedValue(environments)

const result = await service.getEnvironmentsByProjectId('project1')

expect(prisma.environment.findMany).toHaveBeenCalledWith({
where: { projectId: 'project1' },
include: { stage: true },
include: {
stage: true,
_count: { select: { deployments: true } },
},
})
expect(result).toEqual(environments)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { PROD_STAGE_NAME } from './environment.constants'

export type EnvironmentWithCluster = Environment & { cluster: Cluster }
export type EnvironmentWithStage = Environment & { stage: Stage }
export type EnvironmentWithStageAndCount = EnvironmentWithStage & { _count: { deployments: number } }
export type EnvironmentWithDeploymentsCount = EnvironmentWithStage & { deploymentsCount: number }

export interface EnvironmentResourcesSum {
_sum: {
Expand All @@ -25,10 +27,13 @@ export class EnvironmentDatastoreService {
})
}

getEnvironmentsByProjectId(projectId: string): Promise<EnvironmentWithStage[]> {
getEnvironmentsByProjectId(projectId: string): Promise<EnvironmentWithStageAndCount[]> {
return this.prisma.environment.findMany({
where: { projectId },
include: { stage: true },
include: {
stage: true,
_count: { select: { deployments: true } },
},
})
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Cluster, Environment, Project, Stage } from '@prisma/client'
import type { EnvironmentWithCluster, EnvironmentWithStage } from './environment-datastore.service'
import type { EnvironmentWithCluster, EnvironmentWithDeploymentsCount, EnvironmentWithStage, EnvironmentWithStageAndCount } from './environment-datastore.service'
import { faker } from '@faker-js/faker'

export function makeEnvironment(overrides: Partial<Environment> = {}): Environment {
Expand Down Expand Up @@ -85,3 +85,17 @@ export function makeEnvironmentWithStage(overrides: Partial<EnvironmentWithStage
stage: overrides.stage ?? makeStage({ id: base.stageId }),
}
}

export function makeEnvironmentWithStageAndCount(overrides: Partial<EnvironmentWithStageAndCount> = {}): EnvironmentWithStageAndCount {
return {
...makeEnvironmentWithStage(overrides),
_count: overrides._count ?? { deployments: 0 },
}
}

export function makeEnvironmentWithDeploymentsCount(overrides: Partial<EnvironmentWithDeploymentsCount> = {}): EnvironmentWithDeploymentsCount {
return {
...makeEnvironmentWithStage(overrides),
deploymentsCount: overrides.deploymentsCount ?? 0,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Test } from '@nestjs/testing'
import { beforeEach, describe, expect, it } from 'vitest'
import { mockDeep } from 'vitest-mock-extended'
import { ProjectGuard } from '../infrastructure/permission/project/project.guard'
import { makeEnvironment, makeEnvironmentWithStage } from './environment-testing.utils'
import { makeEnvironment, makeEnvironmentWithDeploymentsCount } from './environment-testing.utils'
import { EnvironmentController } from './environment.controller'
import { EnvironmentService } from './environment.service'

Expand Down Expand Up @@ -64,7 +64,7 @@ describe('environmentController', () => {

describe('list', () => {
it('should call environmentService.listByProjectId with projectId', async () => {
const expectedResult = [makeEnvironmentWithStage({ projectId })]
const expectedResult = [makeEnvironmentWithDeploymentsCount({ projectId })]

service.listByProjectId.mockResolvedValue(expectedResult)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { EnvironmentDatastoreService } from './environment-datastore.service'
import {
makeEnvironment,
makeEnvironmentWithCluster,
makeEnvironmentWithStage,
makeEnvironmentWithStageAndCount,
} from './environment-testing.utils'
import { EnvironmentValidationService } from './environment-validation.service'
import { EnvironmentService } from './environment.service'
Expand Down Expand Up @@ -77,13 +77,14 @@ describe('environmentService', () => {

describe('listByProjectId', () => {
it('should return environments by projectId', async () => {
const environments = [makeEnvironmentWithStage({ id: environmentId, projectId })]
const environments = [makeEnvironmentWithStageAndCount({ id: environmentId, projectId, _count: { deployments: 3 } })]
datastore.getEnvironmentsByProjectId.mockResolvedValue(environments)

const result = await service.listByProjectId(projectId)

expect(datastore.getEnvironmentsByProjectId).toHaveBeenCalledWith(projectId)
expect(result).toEqual(environments)
const { _count, ...environment } = environments[0]
expect(result).toEqual([{ ...environment, deploymentsCount: 3 }])
})
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CreateEnvironment, UpdateEnvironment } from '@cpn-console/shared'
import type { Environment } from '@prisma/client'
import type { EventLogAction } from '../events/app-events.service'
import type { EnvironmentWithCluster, EnvironmentWithStage } from './environment-datastore.service'
import type { EnvironmentWithCluster, EnvironmentWithDeploymentsCount } from './environment-datastore.service'
import {
Inject,
Injectable,
Expand All @@ -21,8 +21,12 @@ export class EnvironmentService {
@Inject(AppEventsService) private readonly appEvents: AppEventsService,
) {}

async listByProjectId(projectId: string): Promise<EnvironmentWithStage[]> {
return this.environmentDatastoreService.getEnvironmentsByProjectId(projectId)
async listByProjectId(projectId: string): Promise<EnvironmentWithDeploymentsCount[]> {
const environments = await this.environmentDatastoreService.getEnvironmentsByProjectId(projectId)
return environments.map(({ _count, ...environment }) => ({
...environment,
deploymentsCount: _count.deployments,
}))
}

async createEnvironment(projectId: string, environmentToCreate: CreateEnvironment, userId: string, requestId: string): Promise<Environment> {
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/contracts/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { apiPrefix, apiPrefixV2, contractInstance } from '../api-client.js'
import {
CreateEnvironmentSchema,
EnvironmentSchema,
EnvironmentWithDeploymentsCountSchema,
UpdateEnvironmentSchema,
} from '../schemas/index.js'
import { baseHeaders, ErrorSchema } from './_utils.js'
Expand Down Expand Up @@ -118,7 +119,7 @@ export const environmentContractV2 = contractInstance.router({
summary: 'Get environments',
description: 'Retrieved project environments.',
responses: {
200: EnvironmentSchema.array(),
200: EnvironmentWithDeploymentsCountSchema.array(),
400: ErrorSchema,
401: ErrorSchema,
403: ErrorSchema,
Expand Down
5 changes: 5 additions & 0 deletions packages/shared/src/schemas/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export const EnvironmentSchema = z.object({
autosync: z.boolean(),
}).extend(AtDatesToStringExtend)

export const EnvironmentWithDeploymentsCountSchema = EnvironmentSchema.extend({
deploymentsCount: z.number().int().gte(0),
})

export const CreateEnvironmentSchema = EnvironmentSchema.omit({
id: true,
createdAt: true,
Expand All @@ -36,5 +40,6 @@ export const UpdateEnvironmentSchema = EnvironmentSchema.pick({
})

export type Environment = Zod.infer<typeof EnvironmentSchema>
export type EnvironmentWithDeploymentsCount = Zod.infer<typeof EnvironmentWithDeploymentsCountSchema>
export type CreateEnvironment = Zod.infer<typeof CreateEnvironmentSchema>
export type UpdateEnvironment = Zod.infer<typeof UpdateEnvironmentSchema>