Skip to content

Commit

Permalink
Merge pull request #1040 from cloud-pi-native/feat/repo-sync
Browse files Browse the repository at this point in the history
feat: ✨ allow repo sync from console ui
  • Loading branch information
clairenollet authored Apr 24, 2024
2 parents 3b5b782 + 1cd1c93 commit 09949ab
Show file tree
Hide file tree
Showing 21 changed files with 352 additions and 46 deletions.
2 changes: 1 addition & 1 deletion apps/client/cypress/components/specs/repo-form.ct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('RepoForm.vue', () => {

cy.mount(RepoForm, { props })

cy.get('h1').should('contain', 'Modifier le dépôt')
cy.get('h2').should('contain', 'Modifier le dépôt')
cy.getByDataTestid('repoFieldset').should('have.length', 1)
cy.getByDataTestid('internalRepoNameInput').find('input').should('have.value', props.repo.internalRepoName)
.and('be.disabled')
Expand Down
55 changes: 53 additions & 2 deletions apps/client/cypress/e2e/specs/repos.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ describe('Add repos into project', () => {
cy.url().should('contain', '/repositories')

cy.getByDataTestid('addRepoLink').click({ timeout: 30_000 })
cy.get('h1').should('contain', 'Ajouter un dépôt au projet')
cy.get('h2').should('contain', 'Ajouter un dépôt au projet')
cy.getByDataTestid('addRepoBtn').should('be.disabled')
cy.getByDataTestid('internalRepoNameInput').find('input').clear().type(repo.internalRepoName)
cy.getByDataTestid('addRepoBtn').should('be.disabled')
Expand Down Expand Up @@ -139,7 +139,7 @@ describe('Add repos into project', () => {
cy.wait('@getProjects').its('response').then(response => {
repos = response?.body.find(resProject => resProject.name === project.name).repositories
cy.getByDataTestid(`repoTile-${repos[0].internalRepoName}`).click()
.get('h1').should('contain', 'Modifier le dépôt')
.get('h2').should('contain', 'Modifier le dépôt')
.getByDataTestid('internalRepoNameInput').should('be.disabled')
.getByDataTestid('externalRepoUrlInput').find('input').clear().type('https://github.com/externalUser04/new-repo.git')

Expand All @@ -162,6 +162,57 @@ describe('Add repos into project', () => {
})
})

it('Should synchronise a repo', () => {
cy.intercept('GET', '/api/v1/projects').as('getProjects')
cy.intercept('GET', '/api/v1/projects/*/repositories/*/sync/*').as('syncRepo')
let repos

cy.goToProjects()
.getByDataTestid(`projectTile-${project.name}`).click()
.getByDataTestid('menuRepos').click()
.url().should('contain', '/repositories')

cy.wait('@getProjects').its('response').then(response => {
repos = response?.body.find(resProject => resProject.name === project.name).repositories

cy.getByDataTestid(`repoTile-${repos[0].internalRepoName}`)
.click()

cy.get('h2').should('contain', 'Synchroniser le dépôt')
cy.getByDataTestid('branchNameInput')
.should('have.value', 'main')

cy.getByDataTestid('syncRepoBtn')
.should('be.enabled')
.click()

cy.wait('@syncRepo').its('response.statusCode').should('match', /^20\d$/)

cy.getByDataTestid('snackbar').within(() => {
cy.get('p').should('contain', `Dépôt ${repos[0].internalRepoName} synchronisé`)
})

cy.getByDataTestid('branchNameInput')
.clear()

cy.getByDataTestid('syncRepoBtn')
.should('be.disabled')

cy.getByDataTestid('branchNameInput')
.type('develop')

cy.getByDataTestid('syncRepoBtn')
.should('be.enabled')
.click()

cy.wait('@syncRepo').its('response.statusCode').should('match', /^20\d$/)

cy.getByDataTestid('snackbar').within(() => {
cy.get('p').should('contain', `Dépôt ${repos[0].internalRepoName} synchronisé`)
})
})
})

it('Should generate a GitLab CI for a repo', () => {
cy.intercept('POST', '/api/v1/projects/*/repositories').as('postRepo')
cy.intercept('GET', '/api/v1/projects').as('getProjects')
Expand Down
2 changes: 1 addition & 1 deletion apps/client/cypress/e2e/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Cypress.Commands.add('addRepos', (project, repos) => {

newRepos.forEach((repo) => {
cy.getByDataTestid('addRepoLink').click()
.get('h1').should('contain', 'Ajouter un dépôt au projet')
.get('h2').should('contain', 'Ajouter un dépôt au projet')
.getByDataTestid('internalRepoNameInput').find('input').type(repo.internalRepoName)
.getByDataTestid('externalRepoUrlInput').find('input').clear().type(repo.externalRepoUrl)

Expand Down
7 changes: 6 additions & 1 deletion apps/client/src/api/repositories.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CreateRepositoryBody, UpdateRepositoryBody, RepositoryParams } from '@cpn-console/shared'
import type { CreateRepositoryBody, UpdateRepositoryBody, SyncRepositoryParams, RepositoryParams } from '@cpn-console/shared'
import { apiClient } from './xhr-client.js'

export const addRepo = async (projectId: RepositoryParams['projectId'], data: CreateRepositoryBody) => {
Expand All @@ -11,6 +11,11 @@ export const getRepos = async (projectId: RepositoryParams['projectId']) => {
if (response.status === 200) return response.body
}

export const syncRepository = async (projectId: SyncRepositoryParams['projectId'], repositoryId: SyncRepositoryParams['repositoryId'], branchName: SyncRepositoryParams['branchName']) => {
const response = await apiClient.Repositories.syncRepository({ params: { projectId, repositoryId, branchName } })
return response.body
}

export const updateRepo = async (projectId: RepositoryParams['projectId'], repositoryId: RepositoryParams['repositoryId'], data: UpdateRepositoryBody) => {
if (!data.id) return
const response = await apiClient.Repositories.updateRepository({ body: data, params: { projectId, repositoryId } })
Expand Down
8 changes: 4 additions & 4 deletions apps/client/src/components/RepoForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ const cancel = () => {
data-testid="repo-form"
class="relative"
>
<h1
class="fr-h1 fr-mt-2w"
<h2
class="fr-h2 fr-mt-2w"
>
{{ localRepo.id ? 'Modifier le dépôt' : 'Ajouter un dépôt au projet' }}
</h1>
{{ localRepo.id ? `Modifier le dépôt ${localRepo.internalRepoName}` : 'Ajouter un dépôt au projet' }}
</h2>
<DsfrFieldset
legend="Informations du dépôt"
hint="Les champs munis d\'une astérisque (*) sont requis"
Expand Down
8 changes: 7 additions & 1 deletion apps/client/src/stores/project-repository.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { defineStore } from 'pinia'
import type { CreateRepositoryBody, UpdateRepositoryBody, RepositoryParams } from '@cpn-console/shared'
import api from '@/api/index.js'
import { useProjectStore } from '@/stores/project.js'
import type { CreateRepositoryBody, UpdateRepositoryBody, RepositoryParams } from '@cpn-console/shared'
import { projectMissing } from '@/utils/const.js'

export const useProjectRepositoryStore = defineStore('project-repository', () => {
const projectStore = useProjectStore()

const syncRepository = async (repoId: string, branchName: string) => {
if (!projectStore.selectedProject) throw new Error(projectMissing)
await api.syncRepository(projectStore.selectedProject.id, repoId, branchName)
}

const addRepoToProject = async (newRepo: CreateRepositoryBody) => {
if (!projectStore.selectedProject) throw new Error(projectMissing)
await api.addRepo(projectStore.selectedProject.id, newRepo)
Expand All @@ -29,5 +34,6 @@ export const useProjectRepositoryStore = defineStore('project-repository', () =>
addRepoToProject,
updateRepo,
deleteRepo,
syncRepository,
}
})
71 changes: 63 additions & 8 deletions apps/client/src/views/projects/DsoRepos.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const isOwner = computed(() => project.value?.roles?.some(role => role.userId ==
const repos = ref<RepoTile[]>([])
const selectedRepo = ref<Repo>()
const isNewRepoForm = ref(false)
const branchName = ref<string>('main')
const repoFormId = 'repoFormId'
const syncFormId = 'syncFormId'
const setReposTiles = (project: Project) => {
// @ts-ignore
Expand Down Expand Up @@ -75,6 +79,15 @@ const deleteRepo = async (repoId: Repo['id']) => {
snackbarStore.isWaitingForResponse = false
}
const syncRepository = async () => {
if (!selectedRepo.value) return
if (!branchName.value) branchName.value = 'main'
snackbarStore.isWaitingForResponse = true
projectRepositoryStore.syncRepository(selectedRepo.value.id, branchName.value)
snackbarStore.isWaitingForResponse = false
snackbarStore.setMessage(`Dépôt ${selectedRepo.value.internalRepoName} synchronisé`, 'success')
}
onMounted(() => {
if (!project.value) return
setReposTiles(project.value)
Expand Down Expand Up @@ -149,15 +162,57 @@ watch(project, () => {
@click="setSelectedRepo(repo.data)"
/>
</div>
<RepoForm
<div
v-if="selectedRepo?.internalRepoName === repo.id"
:is-project-locked="project?.locked"
:is-owner="isOwner"
:repo="selectedRepo"
@save="(repo) => saveRepo(repo)"
@delete="(repoId) => deleteRepo(repoId)"
@cancel="cancel()"
/>
>
<DsfrNavigation
class="fr-mb-4w"
:nav-items="[
{
to: `#${syncFormId}`,
text: '#Synchroniser le dépôt'
},
{
to: `#${repoFormId}`,
text: '#Modifier le dépôt'
},
]"
/>
<div
:id="syncFormId"
class="flex flex-col gap-4 fr-mb-4w"
>
<h2
class="fr-h2 fr-mt-2w"
>
Synchroniser le dépôt {{ selectedRepo?.internalRepoName }}
</h2>
<DsfrInput
v-model="branchName"
data-testid="branchNameInput"
label="Branche cible"
label-visible
required
placeholder="main"
/>
<DsfrButton
data-testid="syncRepoBtn"
label="Lancer la synchronisation"
secondary
:disabled="!branchName"
@click="syncRepository()"
/>
</div>
<RepoForm
:id="repoFormId"
:is-project-locked="project?.locked"
:is-owner="isOwner"
:repo="selectedRepo"
@save="(repo) => saveRepo(repo)"
@delete="(repoId) => deleteRepo(repoId)"
@cancel="cancel()"
/>
</div>
</div>
<div
v-if="!repos.length && !isNewRepoForm"
Expand Down
1 change: 1 addition & 0 deletions apps/server/.env.integ-example
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ EXTERNAL_PLUGINS_DIR_HOST_PATH=/path/to/plugins

# GRAFANA_HOST=
# GRAFANA_URL=
# GRAFANA_NAMESPACE=
# MIMIR_URL=
# KEYCLOAK_CLIENT_SECRET_GRAFANA=
# HTTP_PROXY=
Expand Down
27 changes: 26 additions & 1 deletion apps/server/src/resources/repository/business.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Project, Repository, User } from '@prisma/client'
import { type CreateRepositoryBody, type ProjectRoles, type UpdateRepositoryBody } from '@cpn-console/shared'
import { addLogs, deleteRepository as deleteRepositoryQuery, getProjectInfos, getProjectInfosAndRepos, getUserById, initializeRepository, updateRepository as updateRepositoryQuery } from '@/resources/queries-index.js'
import { checkInsufficientRoleInProject, checkRoleAndLocked } from '@/utils/controller.js'
import { BadRequestError, ForbiddenError, NotFoundError, UnauthorizedError, UnprocessableContentError } from '@/utils/errors.js'
import { BadRequestError, DsoError, ForbiddenError, NotFoundError, UnauthorizedError, UnprocessableContentError } from '@/utils/errors.js'
import { hook } from '@/utils/hook-wrapper.js'

export const getRepositoryById = async (
Expand Down Expand Up @@ -38,6 +38,28 @@ export const getProjectAndcheckRole = async (
return project
}

export const syncRepository = async (
projectId: Project['id'],
repositoryId: Repository['id'],
userId: User['id'],
branchName: string,
requestId: string,
) => {
try {
await getProjectAndcheckRole(userId, projectId)

const { results } = await hook.misc.syncRepository(repositoryId, { branchName })

await addLogs('Sync Repository', results, userId, requestId)
if (results.failed) {
throw new UnprocessableContentError('Echec des opérations', undefined)
}
} catch (error) {
if (error instanceof DsoError) throw error
throw new Error('Echec de la synchronisation du dépôt')
}
}

export const checkUpsertRepository = async (
userId: User['id'],
projectId: Project['id'],
Expand Down Expand Up @@ -85,6 +107,7 @@ export const createRepository = async (

return repo
} catch (error) {
if (error instanceof DsoError) throw error
throw new Error('Echec de la création du dépôt')
}
}
Expand Down Expand Up @@ -118,6 +141,7 @@ export const updateRepository = async (

return repo
} catch (error) {
if (error instanceof DsoError) throw error
throw new Error('Echec de la mise à jour du dépôt')
}
}
Expand All @@ -139,6 +163,7 @@ export const deleteRepository = async (
throw new UnprocessableContentError('Echec des opérations', undefined)
}
} catch (error) {
if (error instanceof DsoError) throw error
throw new Error('Echec de la mise à jour du dépôt')
}
}
32 changes: 32 additions & 0 deletions apps/server/src/resources/repository/controllers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,38 @@ describe('Repository routes', () => {
})
})

describe('syncRepositoryController', () => {
it('Should sync a repository', async () => {
const projectInfos = createRandomDbSetup({}).project
projectInfos.roles = [...projectInfos.roles, getRandomRole(getRequestor().id, projectInfos.id, 'owner')]
const repoToSync = projectInfos.repositories[0]
const branchName = 'main'

prisma.project.findUnique.mockResolvedValue(projectInfos)

const response = await app.inject()
.get(`/api/v1/projects/${projectInfos.id}/repositories/${repoToSync.id}/sync/${branchName}`)
.end()

expect(response.statusCode).toEqual(204)
})

it('Should not sync a repository if not project member', async () => {
const projectInfos = createRandomDbSetup({}).project
const repoToSync = projectInfos.repositories[0]
const branchName = 'main'

prisma.project.findUnique.mockResolvedValue(projectInfos)

const response = await app.inject()
.get(`/api/v1/projects/${projectInfos.id}/repositories/${repoToSync.id}/sync/${branchName}`)
.end()

expect(response.statusCode).toEqual(403)
expect(JSON.parse(response.body).error).toEqual('Vous n’avez pas les permissions suffisantes dans le projet')
})
})

// POST
describe('createRepositoryController', () => {
it('Should create a repository', async () => {
Expand Down
13 changes: 13 additions & 0 deletions apps/server/src/resources/repository/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ export const initializeRepository = async ({ projectId, internalRepoName, extern
})
}

export const getHookRepository = (id: Repository['id']) => prisma.repository.findUniqueOrThrow({
where: {
id,
},
include: {
project: {
include: {
organization: true,
},
},
},
})

// UPDATE
export const updateRepository = async (id: Repository['id'], infos: Partial<Repository>) => {
return prisma.repository.update({ where: { id }, data: { ...infos } })
Expand Down
Loading

0 comments on commit 09949ab

Please sign in to comment.