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
46 changes: 46 additions & 0 deletions apps/client/src/components/DeploymentCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<script lang="ts" setup>
import type { Deployment, Stage } from '@cpn-console/shared'

defineProps<{ deployment: Deployment & { stage?: Stage } }>()
</script>

<template>
<div class="fr-card fr-enlarge-link cursor-pointer w-1/4">
<div class="fr-card__body">
<div class="fr-card__content fr-px-4v fr-pb-4v fr-pt-5v">
<p class="font-bold">
{{ deployment.name }}
</p>
<DsfrBadge
class="fr-mb-0"
:label="`${deployment.environment.name}${deployment.stage?.name ? ` • ${deployment.stage.name}` : ''}`"
no-icon
small
type="info"
/>
<p class="fr-text--sm fr-text-mention--grey uppercase font-bold fr-my-4v">
{{ deployment.deploymentSources.length }}
<template v-if="deployment.deploymentSources.length > 1">
dépôts
</template>
<template v-else>
dépôt
</template>
</p>
<div class="flex flex-wrap items-start gap-2">
<div
v-for="source in deployment.deploymentSources"
:key="source.id"
class="px-2 py-1 shadow fr-background-alt--grey flex items-center gap-2 fr-text--sm"
>
<v-icon name="mdi:git" />
{{ source.repository.internalRepoName }}
<span class="text-xs fr-m-0 fr-text-mention--grey font-mono leading-none">
{{ source.targetRevision || 'HEAD' }}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
135 changes: 135 additions & 0 deletions apps/client/src/components/DeploymentModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<script lang="ts" setup>
import type { Cluster, Deployment, Environment, Stage, UpdateDeployment, Zone } from '@cpn-console/shared'
import type { DsfrRadioButtonProps } from '@gouvminint/vue-dsfr'
import type { Project } from '@/utils/project-utils.js'
import { CreateDeploymentSchema, DeploymentSchema } from '@cpn-console/shared'
import { useSnackbarStore } from '@/stores/snackbar.js'

const props = withDefaults(defineProps<{
opened: boolean
environments: (Environment & { cluster?: Cluster, zone?: Zone, stage?: Stage })[]
repoOptions: { text: string, value: string }[]
deployment?: Deployment
project: Project
}>(), { opened: false })

const emit = defineEmits<{ close: [] }>()

const snackbarStore = useSnackbarStore()

const { opened } = toRefs(props)

const options: ComputedRef<Omit<DsfrRadioButtonProps, 'modelValue'>[]> = computed(
() => props.environments.map(env => ({
hint: env.stage?.name,
label: env.name,
value: env.id,
class: 'fr-p-0',
})),
)

const deployment = ref<Partial<UpdateDeployment & { id: string }>>(
props.deployment ? { ...props.deployment } : { projectId: props.project.id, autosync: true },
)

watch(() => props.deployment, (newValue) => {
deployment.value = newValue ? { ...newValue } : { projectId: props.project.id, autosync: true }
}, { deep: true })

const deploymentSourcesModel = computed({
get: () => deployment.value?.deploymentSources ?? [],
set: (value: UpdateDeployment['deploymentSources']) =>
deployment.value = { ...deployment.value, deploymentSources: value },
})

const isLoading = ref(false)

function upsertDeployment() {
if (isLoading.value) return

const body = CreateDeploymentSchema.safeParse(deployment.value)
if (!body.success) {
snackbarStore.setMessage(body.error.message, 'error')
return
}

isLoading.value = true
if (deployment.value.id) {
props.project.Deployments.update(deployment.value.id, body.data)
.then(closeModal)
.catch(reason => snackbarStore.setMessage(reason, 'error'))
.finally(() => isLoading.value = false)
} else {
props.project.Deployments.create(body.data)
.then(closeModal)
.catch(reason => snackbarStore.setMessage(reason, 'error'))
.finally(() => isLoading.value = false)
}
}

function closeModal() {
deployment.value = { projectId: props.project.id, autosync: true }
emit('close')
}
</script>

<template>
<DsfrModal title="" :opened is-alert @close="closeModal">
<div class="w-full">
<h4>
<template v-if="deployment.id">
Modifier le déploiement
</template>
<template v-else>
Ajouter un déploiement au projet
</template>
</h4>

<div class="w-full">
<DsfrInputGroup
v-model="deployment.name"
label="Nom du déploiement"
class="fr-mb-2v"
label-visible
:required="true"
:error-message="!!deployment.name && !DeploymentSchema.pick({ name: true }).safeParse({ name: deployment.name }).success ? `Le nom du déploiement ne doit pas contenir d\'espace, doit être unique pour le projet et le cluster sélectionnés, être en minuscules.` : undefined"
placeholder="deploy0"
/>
</div>

<h6 class="fr-mb-0">
Environnement cible
</h6>
<p class="fr-text--sm fr-text-mention--grey fr-mb-0">
Un déploiement est lié à exactement 1 environnement
</p>
<DsfrRadioButtonSet
v-model="deployment.environmentId"
:options="options"
:rich="true"
:required="true"
/>

<h6 class="fr-mb-0">
Dépôts à inclure
</h6>
<DeploymentRepoSelect v-model="deploymentSourcesModel" :repo-options="repoOptions" />
</div>
<div class="w-full flex justify-end gap-4">
<DsfrButton
label="Enregistrer"
primary
size="md"
:icon="isLoading ? { name: 'ri:loader-4-line', animation: 'spin' } : undefined"
:icon-right="isLoading"
@click="upsertDeployment"
/>
<DsfrButton
label="Annuler"
secondary
size="md"
@click="closeModal"
/>
</div>
</DsfrModal>
</template>
65 changes: 65 additions & 0 deletions apps/client/src/components/DeploymentRepoOption.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
import type { UpdateDeployment } from '@cpn-console/shared'

withDefaults(defineProps<{
cantDelete: boolean
options: { text: string, value: string }[]
}>(), {
options: () => [],
cantDelete: false,
})
defineEmits<{ delete: [] }>()

const model = defineModel<Partial<UpdateDeployment['deploymentSources'][0]>>(
{
default: {
id: undefined,
type: 'git',
repositoryId: undefined,
targetRevision: undefined,
path: undefined,
helmValuesFiles: undefined,
},
},
)
</script>

<template>
<div class="w-full">
<div v-if="!$props.cantDelete" class="flex w-full justify-end">
<DsfrButton icon-only icon="ri:delete-bin-7-line" secondary @click="$emit('delete')" />
</div>
<DsfrSelect v-model="model.repositoryId" label="Dépôt" :options="$props.options" />
<DsfrInputGroup
v-model="model.targetRevision"
class="mb-2"
placeholder="HEAD"
label="Nom de la révision à déployer (branche, tag, commit)"
label-visible
/>
<DsfrInputGroup
v-model="model.path"
class="mb-2"
placeholder="manifest/"
label="Chemin du répertoire à déployer"
label-visible
/>
<DsfrInputGroup
v-model="model.helmValuesFiles"
class="mb-2"
is-textarea
label="Fichiers values (Helm)"
label-visible
hint="Un fichier par ligne, chemin relatif par rapport au répertoire à déployer. La balise <env> sera remplacée par le nom de l'environnement. L'ordre des fichiers est déterminant pour la surcharge des valeurs communes. Champ optionnel."
placeholder="values/extra.yaml
values-<env>/custom.yaml"
/>
</div>
</template>

<style lang="css" scoped>
.fr-select-group,
.fr-input-group {
margin-bottom: .75rem;
}
</style>
60 changes: 60 additions & 0 deletions apps/client/src/components/DeploymentRepoSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts" setup>
import type { UpdateDeployment } from '@cpn-console/shared'

defineProps<{
repoOptions: { text: string, value: string }[]
}>()
const depots = defineModel<Partial<UpdateDeployment['deploymentSources'][0]>[]>({ default: [] })

if (depots.value.length === 0) {
addDepot()
}

function addDepot() {
depots.value = [
...depots.value,
{
id: undefined,
type: 'git',
repositoryId: undefined,
targetRevision: undefined,
path: undefined,
helmValuesFiles: undefined,
},
]
}

function removeDepot(index: number) {
depots.value = depots.value.filter((_, i) => i !== index)
}

function updateDepot(index: number, value: Partial<UpdateDeployment['deploymentSources'][0]>) {
depots.value[index] = value
}
</script>

<template>
<div class="p-2">
<div class="w-full flex flex-col gap-2">
<DeploymentRepoOption
v-for="(depot, index) in depots"
:key="depot.id ?? `new-${index}`"
:model-value="depot"
:options="$props.repoOptions"
class="w-full py-2 px-4 border border-solid border-gray-300"
:cant-delete="index === 0"
@update:model-value="value => updateDepot(index, value)"
@delete="removeDepot(index)"
/>
</div>
<div class="w-full flex mt-4">
<DsfrButton
type="button"
label="Ajouter un dépôt"
icon="ri:add-line"
secondary
@click="addDepot"
/>
</div>
</div>
</template>
78 changes: 78 additions & 0 deletions apps/client/src/components/DeploymentResources.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<script setup lang="ts">
import type { Cluster, Deployment, Environment, Repo, Stage, Zone } from '@cpn-console/shared'
import type { Project } from '@/utils/project-utils.js'

const props = defineProps<{
environments: (Environment & { cluster?: Cluster, zone?: Zone, stage?: Stage })[]
repositories: (Repo & { source: string })[]
project: Project
}>()
const deployments = ref<Deployment[]>([])
const repoOptions = computed(() => props.repositories.map(repo => ({
text: repo.internalRepoName,
value: repo.id,
})))

const deploymentsWithStage = computed(() => deployments.value.map((deployment) => {
const stage = props.environments.find(env => env.id === deployment.environmentId)?.stage
return {
...deployment,
stage,
}
}))

const isModalOpen = ref(false)
const selectedDeployment = ref<Deployment>()

function openModal(deployment?: Deployment) {
selectedDeployment.value = deployment
isModalOpen.value = true
}

function closeModal() {
loadDeployments()
isModalOpen.value = false
}

async function loadDeployments() {
deployments.value = await props.project.Deployments.list()
}

onMounted(loadDeployments)
</script>

<template>
<div class="w-full">
<div class="w-full flex justify-between items-start">
<div>
<h4 class="fr-mb-0">
Déploiements
</h4>
<p>Associez un environnement à un ou plusieurs dépôts pour configurer un déploiment.</p>
</div>
<DsfrButton
label="Ajouter un nouveau déploiement"
tertiary
title="Ajouter un déploiement"
icon="ri:add-line"
@click="() => openModal()"
/>
</div>
<div class="w-full flex items-start gap-4">
<DeploymentCard
v-for="deployment in deploymentsWithStage"
:key="deployment.id"
:deployment="deployment"
@click="() => openModal(deployment)"
/>
</div>
</div>
<DeploymentModal
v-model:opened="isModalOpen"
:environments="props.environments"
:repo-options="repoOptions"
:project="props.project"
:deployment="selectedDeployment"
@close="closeModal"
/>
</template>
Loading