Skip to content
Snippets Groups Projects
Commit 3a4dd91c authored by Erwan Rouchet's avatar Erwan Rouchet
Browse files

Merge branch 'model-version-picker' into 'master'

Allow to pick a model and a model version

See merge request teklia/arkindex/frontend!1334
parents 4b608d78 80f67172
No related branches found
No related tags found
1 merge request!1334Allow to pick a model and a model version
<template>
<div class="field">
<div class="control" v-on:click="opened = true">
<input
class="input"
readonly
:placeholder="placeholder"
:value="modelValue?.name"
/>
</div>
<Modal is-large v-model="opened" :title="placeholder">
<form
v-if="modelsPage"
class="field is-pulled-right"
v-on:submit.prevent="filter"
>
<div class="control">
<input
class="input"
type="text"
v-model="nameFilter"
placeholder="Filter by name…"
/>
</div>
</form>
<div class="title is-4">Available Models</div>
<div class="control">
<Paginator
:response="modelsPage"
v-slot="{ results }"
:loading="loading"
v-model:page="page"
singular="model"
plural="models"
>
<table class="table is-fullwidth is-hoverable">
<thead>
<tr><th>Name</th></tr>
</thead>
<tbody>
<tr v-for="model in results" :key="model.id">
<td>
{{ model.name }}
<button
v-if="model.id !== modelValue?.id"
type="button"
class="button is-pulled-right"
v-on:click="$emit('update:modelValue', model)"
>
Select
</button>
<!-- Highlight the currently selected model -->
<button
v-else
type="button"
class="button is-info is-pulled-right"
>
Selected
</button>
</td>
</tr>
</tbody>
</table>
</Paginator>
</div>
</modal>
</div>
</template>
<script>
import { mapActions, mapMutations } from 'vuex'
import Paginator from '@/components/Paginator.vue'
import { errorParser } from '@/helpers'
import Modal from '@/components/Modal'
/*
* A component allowing to pick a specific model
*/
export default {
components: {
Paginator,
Modal
},
emits: ['update:modelValue'],
props: {
modelValue: {
type: Object,
default: null
},
placeholder: {
type: String,
default: 'Pick a model…'
}
},
data: () => ({
opened: false,
loading: false,
page: 1,
modelsPage: null,
nameFilter: ''
}),
methods: {
...mapMutations('notifications', ['notify']),
...mapActions('model', ['listModels']),
async updateModelsPage () {
this.loading = true
try {
const payload = { page: this.page }
if (this.nameFilter) payload.name = this.nameFilter
this.modelsPage = await this.listModels(payload)
} catch (err) {
this.notify({ type: 'error', text: `An error occurred listing models: ${errorParser(err)}` })
} finally {
this.loading = false
}
},
filter () {
// Update models list. Reset page if required
if (this.page === 1) return this.updateModelsPage()
this.page = 1
}
},
watch: {
page: {
immediate: true,
handler: 'updateModelsPage'
}
}
}
</script>
<template>
<div class="field">
<div class="control" v-on:click="opened = true">
<input
class="input"
readonly
:placeholder="placeholder"
:value="shortId"
/>
</div>
<Modal is-large v-model="opened" :title="placeholder">
<div class="title is-4">Available versions</div>
<div class="control">
<Paginator
:response="versionsPage"
v-slot="{ results }"
:loading="loading"
v-model:page="page"
singular="model version"
plural="model versions"
>
<table class="table is-fullwidth is-hoverable">
<thead>
<th>ID</th>
<th>State</th>
<th>Tag</th>
<th>Created</th>
<th>Parent</th>
<th class="is-narrow">Actions</th>
</thead>
<tbody>
<Row
v-for="version in results"
:key="version.id"
:version="version"
selectable
:selected="version.id === modelValue?.id"
v-on:selected-version="selectVersion"
/>
</tbody>
</table>
</Paginator>
</div>
</modal>
</div>
</template>
<script>
import { mapActions, mapMutations } from 'vuex'
import Row from './Row'
import Paginator from '@/components/Paginator.vue'
import { errorParser } from '@/helpers'
import Modal from '@/components/Modal'
/*
* A component allowing to pick a specific model
*/
export default {
components: {
Row,
Paginator,
Modal
},
emits: ['update:modelValue'],
props: {
modelId: {
type: String,
required: true
},
// The worker version object
modelValue: {
type: Object,
default: null
},
placeholder: {
type: String,
default: 'Pick a model version…'
}
},
data: () => ({
opened: false,
loading: false,
page: 1,
versionsPage: null,
nameFilter: '',
selectedVersion: null
}),
computed: {
shortId () {
return this.modelValue?.id && this.modelValue.id.substring(0, 8)
}
},
methods: {
...mapMutations('notifications', ['notify']),
...mapActions('model', ['listModelVersions']),
async updateVersionsPage () {
this.loading = true
try {
this.versionsPage = await this.listModelVersions({ modelId: this.modelId, page: this.page })
} catch (err) {
this.notify({ type: 'error', text: `An error occurred listing model versions: ${errorParser(err)}` })
} finally {
this.loading = false
}
},
selectVersion (version) {
this.$emit('update:modelValue', version)
}
},
watch: {
modelId: {
immediate: true,
handler: 'updateVersionsPage'
},
page: {
handler: 'updateVersionsPage'
}
}
}
</script>
......@@ -31,16 +31,17 @@
{{ shortParent }}
</div>
</td>
<td v-if="!processId">
<td v-if="!processId && !selectable">
<DeleteModal
:version="version"
/>
</td>
<td v-else>
<button
type="button"
title="Use this model version"
class="button"
:class="{ 'is-primary': version.id === workerRun?.model_version_id , 'is-loading': loading }"
:class="{ 'is-primary': isSelected, 'is-loading': loading }"
v-on:click="useModelVersion"
>
Use
......@@ -59,6 +60,7 @@ export default {
components: {
DeleteModal
},
emits: ['selected-version'],
props: {
version: {
// The corresponding model version
......@@ -77,6 +79,16 @@ export default {
*/
type: String,
default: ''
},
selectable: {
// Make the model version selectable by emitting a selected-version event
type: Boolean,
default: false
},
selected: {
// Display the component as selected while in selectable mode
type: Boolean,
default: false
}
},
data: () => ({
......@@ -101,6 +113,13 @@ export default {
},
workerRun () {
return this.workerRuns?.[this.processId]?.[this.workerRunId]
},
isSelected () {
/*
* Display the version as selected if linked to the worker run
* or marked as selected in selectable mode
*/
return this.selected || this.version.id === this.workerRun?.model_version_id
}
},
methods: {
......@@ -125,6 +144,8 @@ export default {
}
},
async useModelVersion () {
if (this.selectable) this.$emit('selected-version', this.version)
if (!this.workerRunId || !this.processId) return
// Do not do anything if it's still loading or the worker run already uses the selected model version
if (this.loading || this.version.id === this.workerRun?.model_version_id) return
try {
......
import { assert } from 'chai'
import axios from 'axios'
import { mount } from '@vue/test-utils'
import ModelPicker from '@/components/Model/ModelPicker.vue'
import { FakeAxios } from '../testhelpers.js'
import store from '../store/index.spec.js'
import { modelsSample } from '../samples.js'
describe('Model', () => {
describe('ModelPicker.vue', () => {
let mock
before('Setting up Axios mock', () => {
mock = new FakeAxios(axios)
})
afterEach(() => {
mock.reset()
store.reset()
})
after('Removing mocks', () => {
mock.restore()
store.reset()
})
it('Lists available models and allows to pick one', async () => {
mock.onGet('/models/', { page: 1, next: null }).reply(200, modelsSample)
const wrapper = mount(ModelPicker, {
global: { plugins: [store] },
props: {}
})
await store.actionsCompleted()
assert.deepStrictEqual(store.history, [
{
action: 'model/listModels',
payload: { page: 1 }
},
{
mutation: 'model/setModels',
payload: modelsSample.results
}
])
const rows = wrapper.findAll('td')
assert.deepStrictEqual(rows.map(row => row.text()), [
'test-model Select',
'test-model2 Select'
])
rows[1].find('button').trigger('click')
assert.deepStrictEqual(wrapper.emitted('update:modelValue'), [
[modelsSample.results[1]]
])
})
})
})
import { assert } from 'chai'
import axios from 'axios'
import { mount } from '@vue/test-utils'
import ModelVersionPicker from '@/components/Model/Versions/ModelVersionPicker.vue'
import { FakeAxios } from '@/../tests/unit/testhelpers.js'
import store from '@/../tests/unit/store/index.spec.js'
import { modelVersionsSample } from '@/../tests/unit/samples.js'
describe('Model', () => {
describe('ModelPicker.vue', () => {
let mock
before('Setting up Axios mock', () => {
mock = new FakeAxios(axios)
})
afterEach(() => {
mock.reset()
store.reset()
})
after('Removing mocks', () => {
mock.restore()
store.reset()
})
it('Lists available model versions and allows to pick one', async () => {
mock.onGet('/model/model_id/versions/', { page: 1, next: null }).reply(200, modelVersionsSample)
const wrapper = mount(ModelVersionPicker, {
global: { plugins: [store] },
props: {
modelId: 'model_id'
}
})
await store.actionsCompleted()
assert.deepStrictEqual(store.history, [
{
action: 'model/listModelVersions',
payload: { modelId: 'model_id', page: 1 }
},
{
mutation: 'model/setModelVersions',
payload: modelVersionsSample.results
}
])
const rows = wrapper.findAll('tr')
assert.deepStrictEqual(rows.map(row => row.findAll('td').map(v => v.text())), [
// ID (first 8 chars), state, tag, created, parent, actions
['versioni', 'Created', '', '2022-03-30 14:01:34', '', 'Use'],
['versioni', 'Available', '', '2022-03-31 14:01:34', 'versioni', 'Use']
])
rows[0].find('button').trigger('click')
assert.deepStrictEqual(wrapper.emitted('update:modelValue'), [
[modelVersionsSample.results[0]]
])
})
})
})
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