Gitlab is now using https://gitlab.lcsb.uni.lu as it's primary address. Please update your bookmarks. FAQ.

Commit 618d0f0a authored by Sascha Herzinger's avatar Sascha Herzinger
Browse files

New task management

parent 4dd4a75b
Pipeline #7084 passed with stages
in 5 minutes and 38 seconds
import axios from 'axios'
import store from '../store/store'
/**
* The RequestManager class is responsible for the communication with the back end.
......@@ -110,7 +109,6 @@ export default class {
* @returns {AxiosPromise} An ES6 promise.
*/
cancelAnalysis (taskID) {
store.dispatch('unsetTask', taskID)
return this._axios.delete(`/analytics/${taskID}`)
}
......
......@@ -85,25 +85,6 @@ export default {
setFilter: (context, {source, filter, value}) => {
context.commit(types.SET_FILTER, {source, filter, value})
},
/**
* Commits a tasks mutation that will add a new task to the store.
* @param context The context of the action.
* @param taskID The id of the task.
* @param taskName The name of the task.
* @param taskState The current state of the task. (SUBMITTED, SUCCESS, FAILURE, PENDING (not existing))
* @param taskMessage A message in case the task failed.
*/
setTask: (context, {taskID, taskName, taskState, taskMessage}) => {
context.commit(types.SET_TASK, {taskID, taskName, taskState, taskMessage})
},
/**
* Commits a tasks mutation that will remove the task for the given taskID.
* @param context The context of the action.
* @param taskID The id of the task to remove.
*/
unsetTask: (context, taskID) => {
context.commit(types.UNSET_TASK, taskID)
},
/**
* Commits panel states to control them on a global level.
* @param context: The context of the action.
......
export default {
data: state => state.data,
tasks: state => state.tasks,
subsets: state => state.subsets,
requestManager: state => state.requestManager,
chartManager: state => state.chartManager,
......
......@@ -5,8 +5,6 @@ export default {
SET_STATE_MANAGER: 'SET_STATE_MANAGER',
SET_SUBSETS: 'SET_SUBSETS',
SET_FILTER: 'SET_FILTER',
SET_TASK: 'SET_TASK',
UNSET_TASK: 'UNSET_TASK',
SET_CONTROL_PANEL: 'SET_CONTROL_PANEL',
SET_OPTIONS: 'SET_OPTIONS'
}
......@@ -23,17 +23,6 @@ export default {
[types.SET_FILTER] (state, {source, filter, value}) {
Vue.set(state.filters, filter, {source, value})
},
[types.SET_TASK] (state, {taskID, taskName, taskState, taskMessage}) {
// avoid triggering possible watchers if task information remain the same
if (!state.tasks[taskID] ||
state.tasks[taskID].taskState !== taskState ||
state.tasks[taskID].taskMessage !== taskMessage) {
Vue.set(state.tasks, taskID, {taskID, taskName, taskState, taskMessage})
}
},
[types.UNSET_TASK] (state, taskID) {
Vue.delete(state.tasks, taskID)
},
[types.SET_CONTROL_PANEL] (state, options) {
Object.assign(state.controlPanel, options)
},
......
......@@ -8,7 +8,6 @@ Vue.use(Vuex)
const state = {
data: [],
tasks: {},
requestManager: null,
chartManager: null,
stateManager: null,
......
<template>
<div class="fjs-chart" @mousedown.capture="focusControlPanel">
<div class="fjs-init-cover" @click="animate" ref="cover" v-show="showCover">
<div class="fjs-init-cover fjs-cover" @click="animate" ref="cover" v-show="showInitCover">
<div>
<span>Select</span>
</div>
</div>
<div class="fjs-loading-cover fjs-cover" v-show="showLoadingCover">
<loader class="fjs-loader"/>
</div>
<div class="fjs-error-cover fjs-cover" v-show="showErrorCover">
<div>
<span>{{ errorMessage }}</span>
</div>
</div>
<slot/>
</div>
</template>
<script>
import ResizeObserver from 'resize-observer-polyfill'
import Loader from './Loader.vue'
import _ from 'lodash'
export default {
name: 'chart',
components: {Loader},
data () {
return {
observer: null,
......@@ -30,8 +41,19 @@
window.removeEventListener('load', this.resize)
},
computed: {
showCover () {
return this.$parent.$data.__init
showInitCover () {
return !this.showLoadingCover && !this.showErrorCover && this.$parent.$data.__init
},
showErrorCover () {
return !this.showLoadingCover && typeof this.errorMessage !== 'undefined'
},
showLoadingCover () {
const key = _.findKey(this.$parent.$data.__tasks, task => task.state === 'SUBMITTED')
return typeof key !== 'undefined'
},
errorMessage () {
const key = _.findKey(this.$parent.$data.__tasks, task => task.state === 'FAILURE')
return typeof key === 'undefined' ? key : this.$parent.$data.__tasks[key].message
}
},
methods: {
......@@ -68,7 +90,7 @@
position: relative
.fjs-animate
animation: fjs-effect-click 300ms ease-in
.fjs-init-cover
.fjs-cover
display: table
height: 100%
width: 100%
......@@ -76,10 +98,6 @@
top: 0
left: 0
z-index: 10
background: white
cursor: pointer
&:hover
box-shadow: inset 0 0 0 2px #e6e6e6
div
display: table-cell
vertical-align: middle
......@@ -88,4 +106,17 @@
font-size: 1.5em
background: #e6e6e6
padding: 10px 20px 10px 20px
.fjs-init-cover
background: white
cursor: pointer
&:hover
box-shadow: inset 0 0 0 2px #e6e6e6
.fjs-loading-cover
div
span
.fjs-error-cover
div
span
background: #fffe86
padding: 0
</style>
......@@ -11,8 +11,6 @@
</div>
<div class="fjs-panel-content" v-show="expanded">
<slot/>
<hr class="fjs-seperator"/>
<task-view/>
</div>
<div class="fjs-links" v-show="expanded">
<a href="https://github.com/LCSB-BioCore/Fractalis-Issues/issues/new/choose" target="_blank">Report Issues</a>
......@@ -24,7 +22,6 @@
</template>
<script>
import TaskView from './TaskView.vue'
import store from '../../store/store'
import { version } from '../../../package.json'
export default {
......@@ -98,9 +95,6 @@
beforeDestroy () {
const panels = store.getters.controlPanel.panels.filter(panel => panel._uid !== this._uid)
store.dispatch('setControlPanel', {panels})
},
components: {
TaskView
}
}
</script>
......
<template>
<div class="fjs-task-view">
<div class="fjs-state-container"
v-for="task in tasks"
v-if="task.taskState === 'SUBMITTED' || task.taskState === 'FAILURE'">
<loader class="fjs-loader" :style="{opacity: task.taskState === 'SUBMITTED' ? 1 : 0}"/>
<span class="fjs-submitted" v-if="task.taskState === 'SUBMITTED'">{{ task.taskName }}</span>
<span class="fjs-failed" v-else>{{ task.taskName }}: {{ task.taskMessage }}</span>
<span class="fjs-cancel-btn" @click="cancelTask(task.taskID)">&#215;</span>
</div>
</div>
</template>
<script>
import store from '../../store/store'
import Loader from './Loader.vue'
export default {
name: 'task-view',
components: {
Loader
},
computed: {
tasks () {
return store.getters.tasks
}
},
methods: {
cancelTask (taskID) {
store.getters.requestManager.cancelAnalysis(taskID)
}
}
}
</script>
<style lang="sass" scoped>
@import '~assets/base.sass'
.fjs-task-view
display: flex
flex-direction: column
justify-content: flex-start
.fjs-state-container
width: 100%
display: flex
flex-direction: row
justify-content: space-between
margin: 1% 0 1% 0
.fjs-submitted
max-width: 75%
.fjs-failed
color: #f0ff9b
font-size: 0.75em
max-width: 75%
.fjs-cancel-btn
font-size: 1.5em
color: #ff9fa1
cursor: pointer
</style>
......@@ -16,64 +16,54 @@ import store from '../../store/store'
export default {
data () {
return {
__init: true
__init: true,
__tasks: {}
}
},
methods: {
async runAnalysis (taskName, args) {
function timeout (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
async __timeout (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
__removeAnalysis (taskName) {
const task = this.$data.__tasks[taskName]
if (typeof task !== 'undefined') {
store.getters.requestManager.cancelAnalysis(task.id)
this.$delete(this.$data.__tasks, taskName)
}
},
async runAnalysis (taskName, args) {
// discard potential previous analysis
this.__removeAnalysis(taskName)
const rv = await store.getters.requestManager.createAnalysis(taskName, args)
const taskID = rv.data.task_id
store.dispatch('setTask', {
taskID,
taskName,
taskState: 'SUBMITTED'
})
this.$set(this.$data.__tasks, taskName, {id: taskID, state: 'SUBMITTED', message: ''})
let timeWaited = 0
let delay = 200
while (timeWaited <= 900000) { // we wait 15 minutes
await timeout(delay)
await this.__timeout(delay)
timeWaited += delay
delay += 100
delay = delay > 3000 ? 3000 : delay
const rv2 = await store.getters.requestManager.getAnalysisStatus(taskID)
const taskInfo = rv2.data
if (taskInfo.state === 'SUCCESS') {
store.dispatch('setTask', {
taskID,
taskName,
taskState: taskInfo.state
})
this.$data.__tasks[taskName].state = taskInfo.state
this.$data.__init = false // current component is no longer in its initial state
return taskInfo.result
} else if (taskInfo.state === 'FAILURE') {
store.dispatch('setTask', {
taskID,
taskName,
taskState: taskInfo.state,
taskMessage: taskInfo.result
})
this.$data.__tasks[taskName].state = taskInfo.state
this.$data.__tasks[taskName].message = taskInfo.result
throw new Error(taskInfo.result)
} else if (taskInfo.state === 'SUBMITTED') {
store.dispatch('setTask', {
taskID,
taskName,
taskState: taskInfo.state})
this.$data.__tasks[taskName].state = taskInfo.state
} else {
throw new Error(`Analysis Task has unhandled state: ${taskInfo.state}`)
}
}
const error = 'Analysis took too long. Stopped listener.'
store.dispatch('setTask', {
taskID,
taskName,
taskState: 'FAILURE',
taskMessage: error
})
this.$data.__tasks[taskName].state = 'FAILURE'
this.$data.__tasks[taskName].message = error
throw new Error(error)
}
}
......
import Vue from 'vue'
import Chart from '../src/vue/components/Chart.vue'
import RunAnalysis from '../src/vue/mixins/run-analysis'
import RequestManager from '../src/services/request-manager'
import store from '../src/store/store'
describe('Chart component', () => {
let vm
beforeEach(() => {
const SomeChart = Vue.component('some-chart', {
mixins: [RunAnalysis],
components: {Chart},
template: '<chart ref="chart"><div></div></chart>'
})
const Component = Vue.extend(SomeChart)
const propsData = {}
vm = new Component({propsData}).$mount()
const requestManager = new RequestManager({handler: '', dataSource: '', fractalisNode: '', getAuth: () => {}})
store.dispatch('setRequestManager', requestManager)
})
afterAll(() => {
document.body.innerHTML = ''
})
it('shows init cover at start', () => {
expect(vm.$refs.chart.showInitCover).toBe(true)
expect(vm.$refs.chart.showErrorCover).toBe(false)
expect(vm.$refs.chart.showLoadingCover).toBe(false)
})
it('shows loading cover when running analysis', async done => {
spyOn(store.getters.requestManager, 'createAnalysis')
.and.returnValue(Promise.resolve({data: {task_id: 123}}))
spyOn(store.getters.requestManager, 'getAnalysisStatus')
.and.returnValue(Promise.resolve({data: {state: 'SUBMITTED', result: ''}}))
vm.runAnalysis('some-task', {})
await vm.__timeout(500)
vm.$nextTick(() => {
expect(Object.keys(vm.$data.__tasks).length).toBe(1)
expect(vm.$refs.chart.showInitCover).toBe(false)
expect(vm.$refs.chart.showErrorCover).toBe(false)
expect(vm.$refs.chart.showLoadingCover).toBe(true)
done()
})
})
it('shows error cover when error occured', async done => {
spyOn(store.getters.requestManager, 'createAnalysis')
.and.returnValue(Promise.resolve({data: {task_id: 123}}))
spyOn(store.getters.requestManager, 'getAnalysisStatus')
.and.returnValue(Promise.resolve({data: {state: 'FAILURE', result: 'something went wrong'}}))
vm.runAnalysis('some-task', {})
await vm.__timeout(500)
vm.$nextTick(() => {
expect(Object.keys(vm.$data.__tasks).length).toBe(1)
expect(vm.$refs.chart.showInitCover).toBe(false)
expect(vm.$refs.chart.showLoadingCover).toBe(false)
expect(vm.$refs.chart.showErrorCover).toBe(true)
done()
})
})
})
......@@ -92,7 +92,7 @@
const fjs = fractalis.init({
handler: 'demo_tcga_coad',
dataSource: location.protocol + '//' + window.location.host,
fractalisNode: 'https://192.168.99.100/',
fractalisNode: 'http://localhost:5000/',
getAuth () {
return {token: ''}
},
......
......@@ -9,11 +9,14 @@ describe('runAnalysis method', () => {
vm = new Vue({
mixins: [RunAnalysis]
})
const requestManager = new RequestManager(
{handler: '', dataSource: '', fractalisNode: '', getAuth: () => {}})
const requestManager = new RequestManager({handler: '', dataSource: '', fractalisNode: '', getAuth: () => {}})
store.dispatch('setRequestManager', requestManager)
})
afterAll(() => {
document.body.innerHTML = ''
})
it('fails if unknown task state', done => {
spyOn(store.getters.requestManager, 'createAnalysis')
.and.returnValue(Promise.resolve({data: {task_id: 123}}))
......@@ -70,7 +73,20 @@ describe('runAnalysis method', () => {
.catch(() => fail())
})
afterAll(() => {
document.body.innerHTML = ''
it('cancels running tasks with same name', async () => {
spyOn(store.getters.requestManager, 'createAnalysis')
.and.returnValue(Promise.resolve({data: {task_id: 123}}))
spyOn(store.getters.requestManager, 'getAnalysisStatus')
.and.returnValue(Promise.resolve({data: {state: 'SUCCESS', result: 123}}))
spyOn(store.getters.requestManager, 'cancelAnalysis')
await vm.runAnalysis('some-name', {})
expect(Object.keys(vm.$data.__tasks).length).toBe(1)
expect(store.getters.requestManager.cancelAnalysis).toHaveBeenCalledTimes(0)
await vm.runAnalysis('some-name', {})
expect(Object.keys(vm.$data.__tasks).length).toBe(1)
expect(store.getters.requestManager.cancelAnalysis).toHaveBeenCalledTimes(1)
await vm.runAnalysis('other-name', {})
expect(Object.keys(vm.$data.__tasks).length).toBe(2)
expect(store.getters.requestManager.cancelAnalysis).toHaveBeenCalledTimes(1)
})
})
......@@ -42,23 +42,6 @@ describe('store', () => {
})
})
describe('task actions', () => {
it('should have working setTask action', () => {
const task = {taskID: 'A', taskName: 'foo', taskState: 'SUBMITTED'}
store.dispatch('setTask', task)
expect(store.getters.tasks['A']).toBeDefined()
expect(store.getters.tasks['A'].taskName).toEqual(task.taskName)
expect(store.getters.tasks['A'].taskState).toEqual(task.taskState)
})
it('should have working unsetTask action', () => {
const task = {taskID: 'A', taskName: 'foo', taskState: 'SUBMITTED'}
store.dispatch('setTask', task)
store.dispatch('unsetTask', 'A')
expect(store.getters.tasks['A']).not.toBeDefined()
})
})
afterAll(() => {
document.body.innerHTML = ''
})
......
import TaskView from '../src/vue/components/TaskView.vue'
import store, { _resetState } from '../src/store/store'
import Vue from 'vue'
import RequestManager from '../src/services/request-manager'
describe('TaskView', () => {
let vm
beforeEach(() => {
_resetState()
const requestManager = new RequestManager(
{handler: '', dataSource: '', fractalisNode: '', getAuth: () => {}})
store.dispatch('setRequestManager', requestManager)
const Component = Vue.extend(TaskView)
vm = new Component().$mount()
})
it('shows all SUBMITTED or FAILED tasks in store', done => {
Promise.all([
store.dispatch('setTask', {taskID: 1, taskName: 'A', taskState: 'SUBMITTED'}),
store.dispatch('setTask', {taskID: 2, taskName: 'B', taskState: 'SUCCESS'}),
store.dispatch('setTask', {taskID: 3, taskName: 'C', taskState: 'SUBMITTED'}),
store.dispatch('setTask', {taskID: 4, taskName: 'D', taskState: 'FAILURE'}),
store.dispatch('setTask', {taskID: 5, taskName: 'E', taskState: 'YAY'}),
store.dispatch('setTask', {taskID: 6, taskName: 'F', taskState: 'PENDING'})
]).then(() => {
Vue.nextTick(() => {
expect(Object.keys(store.getters.tasks).length).toBe(6)
expect(vm.$el.querySelectorAll('.fjs-state-container').length).toBe(3)
expect(vm.$el.querySelectorAll('.fjs-submitted').length).toBe(2)
expect(vm.$el.querySelectorAll('.fjs-failed').length).toBe(1)
done()
})
})
})
it('cancel button works', done => {
Promise.all([
store.dispatch('setTask', {taskID: 1, taskName: 'A', taskState: 'SUBMITTED'}),
store.dispatch('setTask', {taskID: 2, taskName: 'B', taskState: 'FAILURE'})
]).then(() => {
Vue.nextTick(() => {
expect(Object.keys(store.getters.tasks).length).toBe(2)
expect(vm.$el.querySelectorAll('.fjs-state-container').length).toBe(2)
vm.$el.querySelectorAll('.fjs-cancel-btn').forEach(button => button.click())
Vue.nextTick(() => {
expect(Object.keys(store.getters.tasks).length).toBe(0)
expect(vm.$el.querySelectorAll('.fjs-state-container').length).toBe(0)
done()
})
})
})
})
})
......@@ -49,7 +49,7 @@ module.exports = {
performance: {
hints: false
},
devtool: '#source-map',
devtool: '#inline-source-map',
context: __dirname,
target: 'web',
devServer: {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment