Something went wrong on our end
Status.vue 12.46 KiB
<template>
<main class="container is-fluid">
<h1 class="title">Process status</h1>
<h2 class="subtitle">
<ItemId label="Process ID:" :item-id="id" />
</h2>
<div class="columns">
<NameField :process-id="process.id" />
<div class="column" v-if="corpus && process.corpus">
<strong>Project</strong><br />
<router-link :to="{ name: 'navigation', params: { corpusId: process.corpus } }">{{ corpus.name }}</router-link>
</div>
<div class="column">
<strong>Mode</strong><br />{{ processMode }}
</div>
<div class="column" v-if="process.farm">
<strong>Farm</strong><br />{{ process.farm.name }}
</div>
<div class="column">
<strong>Status</strong><br />{{ processStatus }}
</div>
<div class="column is-narrow" v-if="activeProcess || finishedProcess">
<div class="dropdown is-right is-pulled-right is-hoverable dropdown-menu-min-width">
<div class="dropdown-trigger">
<button
class="button is-info"
>
<span>Actions</span>
<i class="icon-down-open"></i>
</button>
</div>
<div class="dropdown-menu">
<div class="dropdown-content">
<template v-if="finishedProcess">
<router-link
v-if="process.element"
class="dropdown-item"
:to="{ name: 'element-details', params: { id: process.element.id } }"
>
<i class="icon-arrow-right"></i>
View element
</router-link>
<a
class="dropdown-item"
:class="!hasAdminAccess ? 'is-disabled' : ''"
v-if="isVerified"
v-on:click="retry"
:title="hasAdminAccess ? 'Retry this entire process' : 'An admin access is required to retry this process'"
>
<i class="icon-undo"></i>
Retry process
</a>
</template>
<template v-else-if="activeProcess">
<a
class="dropdown-item has-text-danger"
:class="!isVerified ? 'is-disabled' : ''"
v-if="isVerified"
v-on:click="stop"
:title="hasAdminAccess ? 'Stop this process' : 'An admin access is required to stop this process'"
>
<i class="icon-minus"></i>
Stop process
</a>
</template>
<router-link
:to="hasActivities ? { name: 'process-workers-activity', params: { processId: process.id } } : ''"
class="dropdown-item"
:class="!hasActivities ? 'is-disabled' : ''"
:title="hasActivities ? 'Display statistics about workers activity' : 'This process has no workers activity tracking'"
>
<i class="icon-chart"></i>
Workers activity
</router-link>
<router-link
:to="hasConfiguration ? { name: 'process-configure', params: { processId: process.id } } : ''"
class="dropdown-item"
:class="!hasConfiguration ? 'is-disabled' : ''"
:title="configurationTitle"
>
<i class="icon-arrow-left"></i>
Process configuration
</router-link>
<TemplateCreation
:process-id="process.id"
v-if="process.state === 'completed'"
>
<template v-slot:default="{ open }">
<a
class="dropdown-item"
v-on:click="openTemplateModal(open)"
:class="!canCreateTemplate ? 'is-disabled' : ''"
:title="createTemplateTitle"
>
<i class="icon-plus"></i>
Create template
</a>
</template>
</TemplateCreation>
</div>
</div>
</div>
</div>
</div>
<div
class="notification is-danger"
v-if="process.mode === 'template'"
>
This is a process template so there is no detail to display.
</div>
<div
class="notification is-warning"
v-else-if="noTasks"
>
This process has no tasks.
</div>
<div
class="notification is-info"
v-else-if="!processTasks.length"
>
Loading tasks...
</div>
<template v-else>
<div class="multi-progress">
<div
v-for="task in runs[lastRun]"
:key="task.id"
class="progress-block"
:class="taskClass(task)"
>
</div>
</div>
<div class="tabs" v-if="Object.keys(runs).length > 1">
<ul>
<li
v-for="run in Object.keys(runs)"
:key="run"
:class="run === selectedRun.toString() ? 'is-active' : ''"
>
<a v-on:click="selectRun(run)">Run {{ run }}</a>
</li>
</ul>
</div>
<Run
:process-id="process.id"
:selected-run="selectedRun"
/>
</template>
</main>
</template>
<script>
import { groupBy } from 'lodash'
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
import {
PROCESS_MODES,
PROCESS_STATES,
PROCESS_STATE_COLORS,
PROCESS_FINAL_STATES
} from '@/config'
import { corporaMixin } from '@/mixins'
import { errorParser } from '@/helpers'
import ItemId from '@/components/ItemId'
import NameField from '@/components/Process/Status/NameField'
import Run from '@/components/Process/Status/Run'
import TemplateCreation from '@/components/Process/TemplateCreation'
export default {
mixins: [
corporaMixin
],
props: {
id: {
type: String,
required: true
},
selectedRun: {
type: Number,
default: -1
}
},
components: {
ItemId,
NameField,
Run,
TemplateCreation
},
data: () => ({
PENDING_ACTIVITY_STATE: 'pending',
READY_ACTIVITY_STATE: 'ready',
loading: false,
error: ''
}),
mounted () {
if ('Notification' in window) Notification.requestPermission()
},
beforeUnmount () {
this.stopPolling()
},
methods: {
...mapActions('process', ['startPolling']),
...mapMutations('process', ['setProcesses', 'stopPolling']),
...mapMutations('notifications', ['notify']),
async retry () {
if (!this.process.id || this.loading) return
try {
await this.$store.dispatch('process/retryProcess', this.process.id)
await this.$store.dispatch('process/retrieveProcess', this.process.id)
this.$router.push({
name: this.$route.name,
params: {
...this.$route.params,
selectedRun: this.lastRun
}
})
} catch (err) {
this.notify({ type: 'error', text: errorParser(err) })
}
},
async stop () {
if (!this.process.id || this.loading) return
this.loading = true
try {
await this.$store.dispatch('process/stop', this.process)
} catch (err) {
this.notify({ type: 'error', text: errorParser(err) })
} finally {
this.loading = false
}
},
selectRun (newRun) {
if (newRun === this.selectedRun) return
const route = {
/*
* Avoid to use the spread operator on the current route to pass attributes.
* Otherwise the object passed to router.replace or router.push will include
* a `path` key, causing trouble to Vue Router building the new URL. On this
* route, the selected run will be concatenated to the process ID making the
* new URL invalid e.g. if someone loads this page they will get a HTTP 404.
*/
name: this.$route.name,
params: {
...this.$route.params,
selectedRun: newRun
}
}
// If we were accessing this route without a selected run, replace the current route, otherwise push a new route
if (!Number.isFinite(this.selectedRun) || this.selectedRun < 0) this.$router.replace(route)
else this.$router.push(route)
},
taskClass (task) {
return task.state === 'unscheduled' ? '' : PROCESS_STATE_COLORS[task.state].cssClass
},
openTemplateModal (fn) {
if (this.canCreateTemplate) fn()
}
},
computed: {
...mapGetters('auth', ['isVerified']),
...mapState('process', ['processes', 'tasks']),
process () {
return this.processes[this.id] ?? {}
},
finishedProcess () {
return PROCESS_FINAL_STATES.includes(this.process?.state)
},
activeProcess () {
return ['pending', 'running'].includes(this.process?.state)
},
processMode () {
return PROCESS_MODES[this.process?.mode] ?? 'Unknown'
},
processStatus () {
return PROCESS_STATES[this.process?.state] ?? 'Unknown'
},
runs () {
return groupBy(Object.values(this.tasks), t => t.run)
},
lastRun () {
return (this.processTasks.length > 0) ? Math.max(...this.processTasks.map(t => t.run)) : null
},
corpusId () {
return this.process.corpus
},
hasAdminAccess () {
if (this.corpus.id) return this.canAdmin(this.corpus)
return true
},
hasActivities () {
return this.process && this.process.activity_state === this.READY_ACTIVITY_STATE
},
/**
* Only the tasks of the current process, and not all the tasks available in the store.
* This ensures we are not displaying the tasks of multiple processes at once, to prevent race conditions.
*/
processTasks () {
return Object.values(this.tasks).filter(task => this.process.id && this.process.id === task.process_id)
},
/**
* Whether this process has no tasks. Returns `false` if the process tasks are not yet available.
*/
noTasks () {
return this.process._complete === true && !this.processTasks.length
},
hasConfiguration () {
return (['dataset', 'workers'].includes(this.process.mode) && this.hasAdminAccess)
},
configurationTitle () {
if (!['dataset', 'workers'].includes(this.process.mode)) return 'Process configuration is only accessible for dataset and workers processes'
else if (!this.hasAdminAccess) return 'An admin access is required to see this process configuration'
return 'See process configuration'
},
canCreateTemplate () {
return ['workers', 'dataset'].includes(this.process.mode)
},
createTemplateTitle () {
if (!this.canCreateTemplate) return 'Templates can only be created from worker or dataset processes'
return 'Create a template based on this process'
}
},
watch: {
lastRun (newValue) {
// If the lastRun value changes for any reason and is still a valid run number, select it if it is lower than the currently selected run
if (newValue !== null && newValue > 0 && this.selectedRun > newValue) this.selectRun(newValue)
},
selectedRun (newValue) {
// Ensure the selected run is always within the bounds of existing runs
if (this.lastRun !== null && (newValue < 0 || newValue > this.lastRun)) this.selectRun(this.lastRun)
},
processTasks () {
// When the tasks are loaded, if no valid run is selected, pick the last run if it is available
if (this.lastRun !== null && (this.selectedRun < 0 || this.selectedRun > this.lastRun)) this.selectRun(this.lastRun)
},
process: {
immediate: true,
handler (newValue, oldValue) {
// If this is the first time the process is loaded and no run is selected, auto-select the latest run.
if (!oldValue && newValue && this.lastRun !== null && (this.selectedRun < 0 || this.selectedRun > this.lastRun)) this.selectRun(this.lastRun)
if (newValue?.id !== oldValue?.id) {
this.startPolling(newValue.id)
}
// Notify of process state changes
if (!('Notification' in window) || !oldValue || !newValue || oldValue.id !== newValue.id || !newValue.state || oldValue.state === newValue.state) return
const n = new Notification(
`${this.processMode} process ${this.process.name?.trim() || this.process.id}`,
{ body: 'Process status updated to ' + this.processStatus }
)
setTimeout(n.close.bind(n), 5000)
}
}
}
}
</script>
<style scoped>
.graph-container {
padding-top: 2rem;
}
.small-edit-button {
margin-left: 1rem;
height: 1.8rem;
width: 1.8rem;
}
</style>