From 1eb783bb5d78ee7527cb25ffee4bd13a835188fb Mon Sep 17 00:00:00 2001 From: Erwan Rouchet <rouchet@teklia.com> Date: Wed, 27 Mar 2024 13:31:00 +0000 Subject: [PATCH] Select export sources in EE --- src/api/export.ts | 10 ++- .../Corpus/Exports/ExportsModal.vue | 80 +++++++++++++------ src/stores/exports.ts | 28 +++++-- src/types/export.ts | 11 +++ tests/unit/samples.js | 11 +++ tests/unit/stores/exports.spec.js | 27 ++++++- 6 files changed, 131 insertions(+), 36 deletions(-) diff --git a/src/api/export.ts b/src/api/export.ts index ecb439024..152dc2b2f 100644 --- a/src/api/export.ts +++ b/src/api/export.ts @@ -1,15 +1,21 @@ import axios from 'axios' import { PageNumberPagination, UUID } from '@/types' -import { CorpusExport } from '@/types/export' +import { CorpusExport, ExportSource } from '@/types/export' import { PageNumberPaginationParameters, unique } from '.' // List a corpus' exports 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 -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 export const deleteExport = unique( 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) diff --git a/src/components/Corpus/Exports/ExportsModal.vue b/src/components/Corpus/Exports/ExportsModal.vue index 6fd8d2d4e..33c72dc35 100644 --- a/src/components/Corpus/Exports/ExportsModal.vue +++ b/src/components/Corpus/Exports/ExportsModal.vue @@ -20,13 +20,15 @@ <table class="table is-hoverable is-fullwidth"> <tr> <th>Creator</th> + <th v-if="hasFeature('enterprise')">Source</th> <th>State</th> <th>Updated</th> <th class="is-narrow">Actions</th> </tr> <tr v-for="corpusExport in results" :key="corpusExport.id"> <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 class="is-narrow"> <div class="field is-grouped"> @@ -65,25 +67,47 @@ :corpus-export="toDelete" /> <template v-slot:footer> - <button - type="button" - class="button is-primary" - :class="{ 'is-loading': loading }" - :title="buttonTitle" - :disabled="!canStart || loading || undefined" - v-on:click="startExport" - > - Start export - </button> - <button - type="button" - class="button" - :class="{ 'is-loading': loading }" - :disabled="loading || undefined" - v-on:click="load(page)" - > - Refresh - </button> + <div class="columns is-marginless"> + <div class="column is-paddingless"> + <button + type="button" + class="button" + :class="{ 'is-loading': loading }" + :disabled="loading || undefined" + v-on:click="load(page)" + > + Refresh + </button> + </div> + <div class="column is-paddingless is-narrow"> + <div class="field has-addons"> + <div class="control" v-if="hasFeature('enterprise') && isVerified && sources?.length > 0"> + <div class="select is-truncated"> + <select :disabled="loading || sources === null || undefined" v-model="source"> + <option v-for="{ name, last_update } in sources" :key="name" :value="name"> + {{ 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> </Modal> </template> @@ -132,11 +156,12 @@ export default defineComponent({ loading: false, EXPORT_STATES, deleteModal: false, - toDelete: null as CorpusExport | null + toDelete: null as CorpusExport | null, + source: 'default' }), computed: { - ...mapGetters('auth', ['isVerified']), - ...mapState(useExportStore, ['corpusExports']), + ...mapGetters('auth', ['isVerified', 'hasFeature']), + ...mapState(useExportStore, ['corpusExports', 'sources']), title () { if (this.corpus && this.corpus.name) return `Exports of project ${this.corpus.name}` return 'Project exports' @@ -155,7 +180,7 @@ export default defineComponent({ } }, methods: { - ...mapActions(useExportStore, ['list', 'start']), + ...mapActions(useExportStore, ['list', 'listSources', 'start']), async load (page = 1) { this.loading = true try { @@ -168,7 +193,7 @@ export default defineComponent({ if (!this.canStart || this.loading) return this.loading = true try { - await this.start(this.corpusId) + await this.start(this.corpusId, this.source) await this.load() } finally { this.loading = false @@ -192,7 +217,10 @@ export default defineComponent({ watch: { modelValue: { handler (newValue) { - if (newValue) this.load(1) + if (newValue) { + if (this.hasFeature('enterprise') && this.isVerified) this.listSources() + this.load(1) + } }, immediate: true }, diff --git a/src/stores/exports.ts b/src/stores/exports.ts index f83fc8e08..5f2d20667 100644 --- a/src/stores/exports.ts +++ b/src/stores/exports.ts @@ -1,6 +1,6 @@ -import { listExports, startExport, deleteExport } from '@/api' +import { listExports, listExportSources, startExport, deleteExport } from '@/api' import { PageNumberPagination, UUID } from '@/types' -import { CorpusExport } from '@/types/export' +import { CorpusExport, ExportSource } from '@/types/export' import { defineStore } from 'pinia' import { useJobsStore, useNotificationStore } from '.' import { errorParser } from '@/helpers' @@ -9,12 +9,18 @@ interface State { /** * 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', { state: (): State => ({ corpusExports: {}, + sources: null }), actions: { async list (corpusId: UUID, page = 1) { @@ -25,9 +31,9 @@ export const useExportStore = defineStore('exports', { useNotificationStore().notify({ type: 'error', text: errorParser(err) }) } }, - async start (corpusId: UUID) { + async start (corpusId: UUID, source: string = "default") { try { - await startExport(corpusId) + await startExport(corpusId, source) useJobsStore().list() } catch (err) { useNotificationStore().notify({ type: 'error', text: errorParser(err) }) @@ -40,6 +46,14 @@ export const useExportStore = defineStore('exports', { } catch (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 + } +}) diff --git a/src/types/export.ts b/src/types/export.ts index 8a89d8d15..6128bb36b 100644 --- a/src/types/export.ts +++ b/src/types/export.ts @@ -11,4 +11,15 @@ export interface CorpusExport { corpus_id: UUID user: User 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 } diff --git a/tests/unit/samples.js b/tests/unit/samples.js index b04af15a7..35ed4b638 100644 --- a/tests/unit/samples.js +++ b/tests/unit/samples.js @@ -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 = [ { diff --git a/tests/unit/stores/exports.spec.js b/tests/unit/stores/exports.spec.js index 54c2bb03e..dc2052f6a 100644 --- a/tests/unit/stores/exports.spec.js +++ b/tests/unit/stores/exports.spec.js @@ -1,7 +1,7 @@ import { assert } from 'chai' import axios from 'axios' 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' 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' + } + ]) + }) + }) }) }) -- GitLab