Skip to content
Snippets Groups Projects
Commit 1eb783bb authored by Erwan Rouchet's avatar Erwan Rouchet Committed by Bastien Abadie
Browse files

Select export sources in EE

parent 2e1ba122
No related branches found
No related tags found
1 merge request!1657Select export sources in EE
import axios from 'axios' import axios from 'axios'
import { PageNumberPagination, UUID } from '@/types' import { PageNumberPagination, UUID } from '@/types'
import { CorpusExport } from '@/types/export' import { CorpusExport, ExportSource } from '@/types/export'
import { PageNumberPaginationParameters, unique } from '.' import { PageNumberPaginationParameters, unique } from '.'
// List a corpus' exports // List a corpus' exports
export const listExports = unique(async (corpusId: UUID, params: PageNumberPaginationParameters = {}): Promise<PageNumberPagination<CorpusExport>> => (await axios.get(`/corpus/${corpusId}/export/`, { params })).data) export const listExports = unique(async (corpusId: UUID, params: PageNumberPaginationParameters = {}): Promise<PageNumberPagination<CorpusExport>> => (await axios.get(`/corpus/${corpusId}/export/`, { params })).data)
// Start an export on a corpus // Start an export on a corpus
export const startExport = unique(async (corpusId: UUID): Promise<CorpusExport> => (await axios.post(`/corpus/${corpusId}/export/`)).data) export const startExport = unique(async (corpusId: UUID, source: string = "default"): Promise<CorpusExport> => (await axios.post(`/corpus/${corpusId}/export/`, { source })).data)
// Delete a corpus export // Delete a corpus export
export const deleteExport = unique( export const deleteExport = unique(
async (id: UUID) => await axios.delete(`/export/${id}/`) async (id: UUID) => await axios.delete(`/export/${id}/`)
) )
/**
* List the available sources to run exports on.
* Available on Arkindex EE only.
*/
export const listExportSources = unique(async (): Promise<ExportSource[]> => (await axios.get('/export-sources/')).data)
...@@ -20,13 +20,15 @@ ...@@ -20,13 +20,15 @@
<table class="table is-hoverable is-fullwidth"> <table class="table is-hoverable is-fullwidth">
<tr> <tr>
<th>Creator</th> <th>Creator</th>
<th v-if="hasFeature('enterprise')">Source</th>
<th>State</th> <th>State</th>
<th>Updated</th> <th>Updated</th>
<th class="is-narrow">Actions</th> <th class="is-narrow">Actions</th>
</tr> </tr>
<tr v-for="corpusExport in results" :key="corpusExport.id"> <tr v-for="corpusExport in results" :key="corpusExport.id">
<td>{{ corpusExport.user.display_name }}</td> <td>{{ corpusExport.user.display_name }}</td>
<td>{{ EXPORT_STATES[(corpusExport as CorpusExport).state] }}</td> <td v-if="hasFeature('enterprise')">{{ corpusExport.source }}</td>
<td>{{ EXPORT_STATES[corpusExport.state] }}</td>
<td :title="corpusExport.updated">{{ dateAgo(corpusExport.updated) }}</td> <td :title="corpusExport.updated">{{ dateAgo(corpusExport.updated) }}</td>
<td class="is-narrow"> <td class="is-narrow">
<div class="field is-grouped"> <div class="field is-grouped">
...@@ -65,25 +67,47 @@ ...@@ -65,25 +67,47 @@
:corpus-export="toDelete" :corpus-export="toDelete"
/> />
<template v-slot:footer> <template v-slot:footer>
<button <div class="columns is-marginless">
type="button" <div class="column is-paddingless">
class="button is-primary" <button
:class="{ 'is-loading': loading }" type="button"
:title="buttonTitle" class="button"
:disabled="!canStart || loading || undefined" :class="{ 'is-loading': loading }"
v-on:click="startExport" :disabled="loading || undefined"
> v-on:click="load(page)"
Start export >
</button> Refresh
<button </button>
type="button" </div>
class="button" <div class="column is-paddingless is-narrow">
:class="{ 'is-loading': loading }" <div class="field has-addons">
:disabled="loading || undefined" <div class="control" v-if="hasFeature('enterprise') && isVerified && sources?.length > 0">
v-on:click="load(page)" <div class="select is-truncated">
> <select :disabled="loading || sources === null || undefined" v-model="source">
Refresh <option v-for="{ name, last_update } in sources" :key="name" :value="name">
</button> {{ name }}
<template v-if="last_update !== null">
(last updated {{ dateAgo(last_update) }})
</template>
</option>
</select>
</div>
</div>
<div class="control">
<button
type="button"
class="button is-primary"
:class="{ 'is-loading': loading }"
:title="buttonTitle"
:disabled="!canStart || loading || undefined"
v-on:click="startExport"
>
Start export
</button>
</div>
</div>
</div>
</div>
</template> </template>
</Modal> </Modal>
</template> </template>
...@@ -132,11 +156,12 @@ export default defineComponent({ ...@@ -132,11 +156,12 @@ export default defineComponent({
loading: false, loading: false,
EXPORT_STATES, EXPORT_STATES,
deleteModal: false, deleteModal: false,
toDelete: null as CorpusExport | null toDelete: null as CorpusExport | null,
source: 'default'
}), }),
computed: { computed: {
...mapGetters('auth', ['isVerified']), ...mapGetters('auth', ['isVerified', 'hasFeature']),
...mapState(useExportStore, ['corpusExports']), ...mapState(useExportStore, ['corpusExports', 'sources']),
title () { title () {
if (this.corpus && this.corpus.name) return `Exports of project ${this.corpus.name}` if (this.corpus && this.corpus.name) return `Exports of project ${this.corpus.name}`
return 'Project exports' return 'Project exports'
...@@ -155,7 +180,7 @@ export default defineComponent({ ...@@ -155,7 +180,7 @@ export default defineComponent({
} }
}, },
methods: { methods: {
...mapActions(useExportStore, ['list', 'start']), ...mapActions(useExportStore, ['list', 'listSources', 'start']),
async load (page = 1) { async load (page = 1) {
this.loading = true this.loading = true
try { try {
...@@ -168,7 +193,7 @@ export default defineComponent({ ...@@ -168,7 +193,7 @@ export default defineComponent({
if (!this.canStart || this.loading) return if (!this.canStart || this.loading) return
this.loading = true this.loading = true
try { try {
await this.start(this.corpusId) await this.start(this.corpusId, this.source)
await this.load() await this.load()
} finally { } finally {
this.loading = false this.loading = false
...@@ -192,7 +217,10 @@ export default defineComponent({ ...@@ -192,7 +217,10 @@ export default defineComponent({
watch: { watch: {
modelValue: { modelValue: {
handler (newValue) { handler (newValue) {
if (newValue) this.load(1) if (newValue) {
if (this.hasFeature('enterprise') && this.isVerified) this.listSources()
this.load(1)
}
}, },
immediate: true immediate: true
}, },
......
import { listExports, startExport, deleteExport } from '@/api' import { listExports, listExportSources, startExport, deleteExport } from '@/api'
import { PageNumberPagination, UUID } from '@/types' import { PageNumberPagination, UUID } from '@/types'
import { CorpusExport } from '@/types/export' import { CorpusExport, ExportSource } from '@/types/export'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useJobsStore, useNotificationStore } from '.' import { useJobsStore, useNotificationStore } from '.'
import { errorParser } from '@/helpers' import { errorParser } from '@/helpers'
...@@ -9,12 +9,18 @@ interface State { ...@@ -9,12 +9,18 @@ interface State {
/** /**
* Exports by corpora. * Exports by corpora.
*/ */
corpusExports: { [corpusId: UUID]: PageNumberPagination<CorpusExport> }, corpusExports: { [corpusId: UUID]: PageNumberPagination<CorpusExport> }
/**
* Available export sources. Arkindex EE only.
*/
sources: ExportSource[] | null
} }
export const useExportStore = defineStore('exports', { export const useExportStore = defineStore('exports', {
state: (): State => ({ state: (): State => ({
corpusExports: {}, corpusExports: {},
sources: null
}), }),
actions: { actions: {
async list (corpusId: UUID, page = 1) { async list (corpusId: UUID, page = 1) {
...@@ -25,9 +31,9 @@ export const useExportStore = defineStore('exports', { ...@@ -25,9 +31,9 @@ export const useExportStore = defineStore('exports', {
useNotificationStore().notify({ type: 'error', text: errorParser(err) }) useNotificationStore().notify({ type: 'error', text: errorParser(err) })
} }
}, },
async start (corpusId: UUID) { async start (corpusId: UUID, source: string = "default") {
try { try {
await startExport(corpusId) await startExport(corpusId, source)
useJobsStore().list() useJobsStore().list()
} catch (err) { } catch (err) {
useNotificationStore().notify({ type: 'error', text: errorParser(err) }) useNotificationStore().notify({ type: 'error', text: errorParser(err) })
...@@ -40,6 +46,14 @@ export const useExportStore = defineStore('exports', { ...@@ -40,6 +46,14 @@ export const useExportStore = defineStore('exports', {
} catch (err) { } catch (err) {
useNotificationStore().notify({ type: 'error', text: errorParser(err) }) useNotificationStore().notify({ type: 'error', text: errorParser(err) })
} }
},
async listSources () {
if (Array.isArray(this.sources)) return
try {
this.sources = await listExportSources()
} catch (err) {
useNotificationStore().notify({ type: 'error', text: errorParser(err) })
}
} }
}, }
}) })
\ No newline at end of file
...@@ -11,4 +11,15 @@ export interface CorpusExport { ...@@ -11,4 +11,15 @@ export interface CorpusExport {
corpus_id: UUID corpus_id: UUID
user: User user: User
state: CorpusExportState state: CorpusExportState
source: string
}
export interface ExportSource {
name: string
/**
* Date of the database backup.
* A value of `null` means either the database is live and not a backup,
* or the backup is missing the metadata providing its date.
*/
last_update: string | null
} }
...@@ -593,6 +593,17 @@ export const exportSample = { ...@@ -593,6 +593,17 @@ export const exportSample = {
} }
} }
export const exportSourcesSample = [
{
name: 'default',
last_update: null
},
{
name: 'export',
last_update: '2038-01-19T03:14:07.999999Z'
}
]
export const bucketsSample = export const bucketsSample =
[ [
{ {
......
import { assert } from 'chai' import { assert } from 'chai'
import axios from 'axios' import axios from 'axios'
import { FakeAxios, setUpTestPinia, actionsCompletedPlugin } from '../testhelpers' import { FakeAxios, setUpTestPinia, actionsCompletedPlugin } from '../testhelpers'
import { jobsSample, exportSample } from '../samples.js' import { jobsSample, exportSample, exportSourcesSample } from '../samples.js'
import { useExportStore, useJobsStore, useNotificationStore } from '@/stores' import { useExportStore, useJobsStore, useNotificationStore } from '@/stores'
describe('exports', () => { describe('exports', () => {
...@@ -117,5 +117,30 @@ describe('exports', () => { ...@@ -117,5 +117,30 @@ describe('exports', () => {
]) ])
}) })
}) })
describe('listSources', () => {
it('lists export sources', async () => {
mock.onGet('/export-sources/').reply(200, exportSourcesSample)
await store.listSources()
assert.deepStrictEqual(store.sources, exportSourcesSample)
})
it('handles errors', async () => {
mock.onGet('/export-sources/').reply(400, { detail: 'oh no' })
await store.listSources()
assert.strictEqual(store.sources, null)
assert.deepStrictEqual(notificationStore.notifications, [
{
id: 0,
type: 'error',
text: 'oh no'
}
])
})
})
}) })
}) })
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