Skip to content
Snippets Groups Projects
Commit 127d1bed authored by ml bonhomme's avatar ml bonhomme :bee: Committed by Erwan Rouchet
Browse files

No more frontend repository management

parent 072eba7b
No related branches found
No related tags found
1 merge request!1671No more frontend repository management
Showing with 1 addition and 557 deletions
......@@ -20,7 +20,6 @@ export * from './mlresults'
export * from './model'
export * from './ponos'
export * from './process'
export * from './repository'
export * from './rights'
export * from './search'
export * from './selection.js'
......
import axios from 'axios'
import { PageNumberPaginationParameters, unique } from '.'
import { PageNumberPagination, UUID } from '@/types'
import { Repository } from '@/types/worker'
// List repositories imported on Arkindex
export const listRepositories = unique(async (params: PageNumberPaginationParameters): Promise<PageNumberPagination<Repository>> => (await axios.get('/process/repos/', { params })).data)
// Retrieve a repository
export const retrieveRepository = unique(async (id: UUID): Promise<Repository> => (await axios.get(`/process/repos/${id}/`)).data)
// Delete a repository
export const deleteRepository = unique((id: UUID) => axios.delete(`/process/repos/${id}/`))
......@@ -27,7 +27,6 @@ type MembershipContentTypeParameters = [
{ corpus: UUID },
{ group: UUID },
{ model: UUID },
{ repository: UUID },
{ worker: UUID }
]
......
......@@ -119,7 +119,6 @@ export default {
// User probably removed their own access, redirect depending on the content type
const route = { name: 'corpus-list' }
if (this.contentType === 'group') route.name = 'user-profile'
else if (this.contentType === 'repository') route.name = 'repos-list'
this.$router.replace(route)
} else {
this.notify({ type: 'error', text: `An error occurred listing members: ${errorParser(err)}` })
......
......@@ -82,9 +82,6 @@
>
<span>Models</span>
</router-link>
<router-link :to="{ name: 'repos-list' }" class="navbar-item" active-class="is-active">
Repositories
</router-link>
<a href="/admin/" class="navbar-item" v-if="isAdmin">
Admin
</a>
......
<template>
<Modal
:model-value="modal"
:title="`Delete ${repo.url}`"
v-on:update:model-value="$emit('update:modal', false)"
>
<p>
Are you sure you want to delete repository <strong>{{ repo.url }}</strong>?
</p>
<br />
<p>
This action is irreversible and will remove:
<ul class="list">
<li v-if="workers">all workers imported from this repository and all their versions (<strong>{{ workers }}</strong>)</li>
<li>all references to this repository from any resource it created</li>
</ul>
</p>
<br />
<p>Note: this will not affect resources created from this repository nor its workers (e.g. elements, classifications, metadata, transcriptions, entities).</p>
<template v-slot:footer="{ close }">
<span
class="button"
v-on:click="close"
title="Cancel deletion"
>
Cancel
</span>
<span
class="button is-danger"
:disabled="loading || null"
:class="{ 'is-loading': loading }"
title="Delete this repository, its associated workers and versions"
v-on:click="remove"
>
<i class="icon-trash"></i>
Delete
</span>
</template>
</Modal>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { mapActions } from 'pinia'
import { errorParser } from '@/helpers'
import { useRepositoryStore, useNotificationStore } from '@/stores'
import { Repository } from '@/types/worker'
import Modal from '@/components/Modal.vue'
export default defineComponent({
components: {
Modal
},
emits: {
'update:modal': (value: boolean) => typeof value === 'boolean',
delete: (value: boolean) => typeof value === 'boolean'
},
props: {
modal: {
type: Boolean,
required: true
},
repo: {
type: Object as PropType<Repository>,
required: true
}
},
data: () => ({
loading: false
}),
methods: {
...mapActions(useRepositoryStore, ['deleteRepository']),
...mapActions(useNotificationStore, ['notify']),
async remove () {
this.loading = true
try {
await this.deleteRepository(this.repo.id)
// Redirect the user to the repositories list
this.$emit('delete', true)
} catch (err) {
this.notify({ type: 'error', text: `An error occurred deleting the repository: ${errorParser(err)}` })
} finally {
this.loading = false
}
}
},
computed: {
workers () {
if (!this.repo.workers.length) return
return this.repo.workers.map(w => w.name).join(', ')
}
}
})
</script>
<style scoped>
ul {
list-style: inside;
}
</style>
<template>
<tr>
<td>
<a :href="repo.url" target="_blank">{{ repo.url }}</a>
</td>
<td>
<template v-if="!repo.workers.length">
<i class="notification is-paddingless is-info">No worker available</i>
</template>
<div v-else v-for="worker in repo.workers" :key="worker.id">
<router-link :to="{ name: 'worker-manage', params: { workerId: worker.id } }">
{{ worker.name }}
</router-link>
</div>
</td>
<td v-if="hasFeature('enterprise')">
<router-link :to="{ name: 'repo-rights', params: { repoId: repo.id } }">
{{ repo.authorized_users }}&nbsp;user<template v-if="repo.authorized_users > 1">s</template>
</router-link>
</td>
<td class="shrink" v-if="isVerified">
<div class="field">
<p class="control">
<button
class="button is-danger"
title="Delete this repository"
v-on:click="$emit('remove', repo)"
>
<i class="icon-trash"></i>
</button>
</p>
</div>
</td>
</tr>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { mapGetters } from 'vuex'
import { Repository } from '@/types/worker'
export default defineComponent({
emits: {
remove (value: Repository) {
return value.id !== undefined
}
},
props: {
repo: {
type: Object as PropType<Repository>,
required: true
}
},
computed: {
...mapGetters('auth', ['isVerified', 'hasFeature'])
}
})
</script>
......@@ -26,8 +26,6 @@ const CorpusList = () => import(/* webpackChunkName: "users" */ '@/views/Corpus/
const CorpusDetails = () => import(/* webpackChunkName: "users" */ '@/views/Corpus/Main')
const ImportFromFiles = () => import(/* webpackChunkName: "users" */ '@/views/Imports/ImportFromFiles')
const ImportFromBucket = () => import(/* webpackChunkName: "users" */ '@/views/Imports/ImportFromBucket')
const ReposList = () => import(/* webpackChunkName: "users" */ '@/views/Repos/List')
const ReposRights = () => import(/* webpackChunkName: "users" */ '@/views/Repos/Rights')
const ProcessList = () => import(/* webpackChunkName: "users" */ '@/views/Process/List')
const ModelsList = () => import(/* webpackChunkName: "users" */ '@/views/Model/List')
const Model = () => import(/* webpackChunkName: "users" */ '@/views/Model')
......@@ -144,13 +142,6 @@ const routes = [
meta: { requiresLogin: true, requiresVerified: true },
props: true
},
{
path: '/process/repos',
name: 'repos-list',
component: ReposList,
meta: { requiresLogin: true, requiresVerified: true },
props: true
},
{
path: `/dataset/:datasetId(${UUID})`,
name: 'dataset-details',
......@@ -158,17 +149,6 @@ const routes = [
meta: { requiresLogin: true, requiresVerified: true },
props: true
},
{
path: `/process/repos/:repoId(${UUID})/rights`,
name: 'repo-rights',
component: ReposRights,
meta: {
requiresLogin: true,
requiresVerified: true,
requiresFeatures: ['enterprise']
},
props: true
},
{
path: '/process/workers',
name: 'workers-list',
......
......@@ -14,7 +14,6 @@ import {
useRightsStore,
useSearchStore,
useWorkerStore,
useRepositoryStore,
useMetaDataStore
} from '@/stores'
......@@ -54,7 +53,6 @@ export const piniaStores = [
useRightsStore,
useSearchStore,
useWorkerStore,
useRepositoryStore,
useMetaDataStore
]
......
......@@ -15,5 +15,4 @@ export { useDatasetStore } from './dataset'
export { useExportStore } from './exports'
export { useEntityStore } from './entity'
export { useTranscriptionStore } from './transcription'
export { useRepositoryStore } from './repos'
export { useMetaDataStore } from './metadata'
import { defineStore } from 'pinia'
import * as api from '@/api'
import { useWorkerStore } from '@/stores'
import { UUID } from '@/types'
import { Repository, WorkerLight } from '@/types/worker'
interface State {
/**
* Repository details mapped by ID
*/
repositories: {
[repositoryId: UUID]: Repository
}
}
export const useRepositoryStore = defineStore('repos', {
state: (): State => ({
repositories: {}
}),
actions: {
async retrieveRepository (id: UUID) {
const repo = await api.retrieveRepository(id)
this.repositories[repo.id] = repo
},
async listRepository (page = 1) {
// List repositories imported on Arkindex
const data = await api.listRepositories({ page })
this.repositories = {
...this.repositories,
...Object.fromEntries(data.results.map((repo: Repository) => [repo.id, repo]))
}
// Set workers in the appropriate store to retrieve them following a link later
const workerStore = useWorkerStore()
const workers = data.results.flatMap(({ workers }) => workers)
workerStore.workers = {
...workerStore.workers,
...Object.fromEntries(workers.map((worker: WorkerLight) => [worker.id, worker]))
}
return data
},
async deleteRepository (id: UUID) {
const data = await api.deleteRepository(id)
delete this.repositories[id]
return data
}
}
})
......@@ -55,10 +55,3 @@ export type WorkerVersion = {
revision: RevisionWithRefs
version: null
})
export interface Repository {
id: UUID,
url: string,
workers: Array<WorkerLight>,
authorized_users: number
}
......@@ -160,7 +160,7 @@ import {
import { errorParser } from '@/helpers'
import { truncateMixin } from '@/mixins'
import { useWorkerStore, useNotificationStore, useRepositoryStore } from '@/stores'
import { useWorkerStore, useNotificationStore } from '@/stores'
import VersionList from '@/components/Process/Workers/Versions/List'
import Paginator from '@/components/Paginator.vue'
import ListMembers from '@/components/Memberships/ListMembers.vue'
......@@ -234,7 +234,6 @@ export default {
}),
computed: {
...mapState(useWorkerStore, ['workerTypes']),
...mapState(useRepositoryStore, ['repos']),
...mapVuexGetters('auth', ['hasFeature']),
readMoreText () {
if (this.expandDescription) return 'collapse description'
......
<template>
<main class="container is-fluid">
<h1 class="title">Repositories</h1>
<h2 class="subtitle">Manage Git repositories</h2>
<Paginator
:response="reposPage"
v-slot="{ results }"
:loading="loading"
>
<table class="table is-fullwidth is-hoverable">
<thead>
<tr>
<th>URL</th>
<th>Workers</th>
<th v-if="hasFeature('enterprise')">Users</th>
<th v-if="isVerified">Actions</th>
</tr>
</thead>
<tbody>
<Row
v-for="repo in results"
:key="repo.id"
:repo="repo"
v-on:remove="repoDeletion = repo"
/>
</tbody>
</table>
</Paginator>
<DeleteModal
v-if="repoDeletion"
v-model:modal="deleteModal"
:repo="repoDeletion"
v-on:delete="handleDeletion"
/>
</main>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useRepositoryStore, useNotificationStore } from '@/stores'
import { mapActions, mapState } from 'pinia'
import { mapGetters } from 'vuex'
import { errorParser } from '@/helpers'
import Paginator from '@/components/Paginator.vue'
import Row from '@/components/Repos/Row.vue'
import DeleteModal from '@/components/Repos/DeleteModal.vue'
import { PageNumberPagination } from '@/types'
import { Repository } from '@/types/worker'
const Component = defineComponent({
components: {
Paginator,
Row,
DeleteModal
},
data: () => ({
loading: false,
// Stores a repository to delete. Automatically prompt the deletion modal
repoDeletion: null as Repository | null,
deleteModal: false,
reposPage: null as PageNumberPagination<Repository> | null
}),
beforeRouteEnter ({ query }, from, next) {
/*
* In order to not get typing error related to 'vm', we name the component which allos us to tell TypeScript that vm
* is of the same type as that component.
*/
next(vm => (vm as InstanceType<typeof Component>).updatePage(parseInt(`${query.page ?? 1}`)))
},
beforeRouteUpdate ({ query }) {
this.updatePage(parseInt(`${query.page ?? 1}`))
},
computed: {
...mapGetters('auth', ['isVerified', 'hasFeature']),
...mapState(useRepositoryStore, ['repositories'])
},
methods: {
...mapActions(useNotificationStore, ['notify']),
...mapActions(useRepositoryStore, ['listRepository']),
async updatePage (page: number) {
this.loading = true
try {
this.reposPage = await this.listRepository(page)
} catch (err) {
this.notify({ type: 'error', text: `An error occurred listing repositories: ${errorParser(err)}` })
} finally {
this.loading = false
}
},
handleDeletion () {
// Reload repositories page
this.repoDeletion = null
this.updatePage(parseInt(`${this.$route.query.page ?? 1}`))
}
},
watch: {
repoDeletion (repo) {
if (repo) this.deleteModal = true
},
deleteModal (open) {
if (!open) this.repoDeletion = null
}
}
})
export default Component
</script>
<template>
<main class="container is-fluid">
<div v-if="loading" class="notification is-info">Loading...</div>
<div v-else-if="!repo" class="notification is-danger">Not found.</div>
<template v-else>
<div class="level">
<div class="level-left is-block">
<div class="subtitle is-5">Repository</div>
<h1 class="title">{{ repo.url }}</h1>
</div>
<div
class="level-right button is-danger"
v-on:click="deleteModal = true"
>
<i class="icon-trash"></i>
Delete
</div>
</div>
<hr />
<h2 class="title is-4">Members</h2>
<ListMembers content-type="repository" :content-id="repoId" />
<DeleteModal
v-model:modal="deleteModal"
:repo="repo"
v-on:delete="postDelete"
/>
</template>
</main>
</template>
<script lang="ts">
import { PropType, defineComponent } from 'vue'
import { UUID } from '@/types'
import { useNotificationStore, useRepositoryStore } from '@/stores'
import { mapState, mapActions } from 'pinia'
import { errorParser } from '@/helpers'
import ListMembers from '@/components/Memberships/ListMembers.vue'
import DeleteModal from '@/components/Repos/DeleteModal.vue'
import { Repository } from '@/types/worker'
export default defineComponent({
components: {
ListMembers,
DeleteModal
},
props: {
repoId: {
type: String as PropType<UUID>,
required: true
}
},
data: () => ({
loading: false,
deleteModal: false
}),
created () {
if (!this.repo) this.load()
},
computed: {
...mapState(useRepositoryStore, ['repositories']),
repo (): Repository {
return this.repositories[this.repoId]
}
},
methods: {
...mapActions(useRepositoryStore, ['retrieveRepository']),
...mapActions(useNotificationStore, ['notify']),
async load () {
try {
this.loading = true
await this.retrieveRepository(this.repoId)
} catch (err) {
this.notify({ type: 'error', text: `An error occurred retrieving the repository: ${errorParser(err)}` })
} finally {
this.loading = false
}
},
postDelete () {
// Replace the route as the repository does not exists anymore
this.$router.replace({ name: 'repos-list' })
}
}
})
</script>
......@@ -215,22 +215,6 @@ export const farmsSample = makeSampleResults([
name: 'Corn farm'
}])
export const repoSample = {
id: 'repoid',
url: 'http://repo',
corpus: 'himanis',
workers: [{ id: 'worker 1' }]
}
export const reposSample = makeSampleResults([repoSample])
export const providersSample = [
{
name: 'GitLabProvider',
display_name: 'GitLab'
}
]
export const credentialsSample = makeSampleResults([
{
id: 'credid',
......
import axios from 'axios'
import { assert } from 'chai'
import { pick } from 'lodash'
import { setActivePinia, createPinia } from 'pinia'
import { useWorkerStore, useRepositoryStore } from '@/stores'
import { reposSample, repoSample } from '../samples'
import { FakeAxios } from '../testhelpers'
describe('repos', () => {
describe('actions', () => {
let mock, store, workerStore
before('Setting up Axios mock', () => {
mock = new FakeAxios(axios)
setActivePinia(createPinia())
store = useRepositoryStore()
workerStore = useWorkerStore()
})
afterEach(() => {
// Remove any handlers, but leave mocking in place
mock.reset()
store.$reset()
workerStore.$reset()
})
after('Removing Axios mock', () => {
// Remove mocking entirely
mock.restore()
})
describe('listRepository', () => {
it('lists existing repositories', async () => {
const sample = {
...reposSample,
count: 2,
results: [
...reposSample.results,
{
id: 'other_repo',
workers: [
{
id: 'worker 2'
}, {
id: 'worker 3'
}
]
}
]
}
mock.onGet('/process/repos/').reply(200, sample)
const page = await store.listRepository()
assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
{
method: 'get',
url: '/process/repos/'
}
])
assert.deepStrictEqual(page, sample)
assert.deepStrictEqual(Object.keys(workerStore.workers), ['worker 1', 'worker 2', 'worker 3'])
})
})
describe('deleteRepository', () => {
it('deletes a repo', async () => {
mock.onDelete('/process/repos/repoid/').reply(204)
store.repositories = { repoid: {}, secondId: {} }
await store.deleteRepository('repoid')
assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
{
method: 'delete',
url: '/process/repos/repoid/'
}
])
})
})
describe('retrieveRepository', () => {
it('retrieves a repo', async () => {
mock.onGet('/process/repos/repoid/').reply(200, repoSample)
await store.retrieveRepository('repoid')
assert.deepStrictEqual(store.repositories, {
repoid: repoSample
})
})
})
})
})
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment