Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update GScan incrementally, using tree/lookup #802

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/components/cylc/common/deltas.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ import { mergeWithCustomizer } from '@/components/cylc/common/merge'
const KEYS = ['workflow', 'cyclePoints', 'familyProxies', 'taskProxies', 'jobs']

/**
* @param {DeltasAdded|Object} added
* @param {DeltasAdded} added
* @param {Object.<String, Object>} lookup
* @return {Result}
*/
Expand Down Expand Up @@ -126,7 +126,7 @@ function applyDeltasAdded (added, lookup) {
/**
* Deltas updated.
*
* @param updated {DeltasUpdated|Object} updated
* @param updated {DeltasUpdated} updated
* @param {Object.<String, Object>} lookup
* @return {Result}
*/
Expand Down Expand Up @@ -164,7 +164,7 @@ function applyDeltasUpdated (updated, lookup) {
/**
* Deltas pruned.
*
* @param {DeltasPruned|Object} pruned - deltas pruned
* @param {DeltasPruned} pruned - deltas pruned
* @param {Object.<String, Object>} lookup
* @return {Result}
*/
Expand Down
113 changes: 25 additions & 88 deletions src/components/cylc/gscan/GScan.vue
Original file line number Diff line number Diff line change
Expand Up @@ -143,42 +143,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</v-flex>
<!-- We check the latestStateTasks below as offline workflows won't have a latestStateTasks property -->
<v-flex
v-if="scope.node.type === 'workflow' && scope.node.node.latestStateTasks"
v-if="scope.node.node.latestStateTasks"
class="text-right c-gscan-workflow-states"
>
<!-- task summary tooltips -->
<span
v-for="[state, tasks] in getLatestStateTasks(Object.entries(scope.node.node.latestStateTasks))"
:key="`${scope.node.id}-summary-${state}`"
:class="getTaskStateClasses(scope.node.node, state)"
>
<v-tooltip color="black" top>
<template v-slot:activator="{ on }">
<!-- a v-tooltip does not work directly set on Cylc job component, so we use a dummy button to wrap it -->
<!-- NB: most of the classes/directives in these button are applied so that the user does not notice it is a button -->
<v-btn
v-on="on"
class="ma-0 pa-0"
min-width="0"
min-height="0"
style="font-size: 120%; width: auto"
:ripple="false"
dark
icon
>
<job :status="state" />
</v-btn>
</template>
<!-- tooltip text -->
<span>
<span class="grey--text">{{ countTasksInState(scope.node.node, state) }} {{ state }}. Recent {{ state }} tasks:</span>
<br/>
<span v-for="(task, index) in tasks.slice(0, maximumTasksDisplayed)" :key="index">
{{ task }}<br v-if="index !== tasks.length -1" />
</span>
</span>
</v-tooltip>
</span>
<WorkflowStateSummary
:node-id="scope.node.id"
:latest-state-tasks="scope.node.node.latestStateTasks"
:state-totals="scope.node.node.stateTotals"
/>
</v-flex>
</v-layout>
</v-list-item-title>
Expand All @@ -204,20 +176,20 @@ import subscriptionComponentMixin from '@/mixins/subscriptionComponent'
import TaskState from '@/model/TaskState.model'
import SubscriptionQuery from '@/model/SubscriptionQuery.model'
import { WorkflowState } from '@/model/WorkflowState.model'
import Job from '@/components/cylc/Job'
import Tree from '@/components/cylc/tree/Tree'
import WorkflowIcon from '@/components/cylc/gscan/WorkflowIcon'
import { addNodeToTree, createWorkflowNode } from '@/components/cylc/gscan/nodes'
import WorkflowStateSummary from '@/components/cylc/gscan/WorkflowStateSummary'
// import { addNodeToTree, createWorkflowNode } from '@/components/cylc/gscan/nodes'
import { filterHierarchically } from '@/components/cylc/gscan/filters'
import GScanCallback from '@/components/cylc/gscan/callbacks'
import { GSCAN_DELTAS_SUBSCRIPTION } from '@/graphql/queries'

export default {
name: 'GScan',
components: {
Job,
Tree,
WorkflowIcon
WorkflowIcon,
WorkflowStateSummary
},
mixins: [
subscriptionComponentMixin
Expand Down Expand Up @@ -313,16 +285,16 @@ export default {
}
},
computed: {
...mapState('workflows', ['workflows']),
workflowNodes () {
// NOTE: In case we decide to allow the user to switch between hierarchical and flat
// gscan view, then all we need to do is just pass a boolean data-property to
// the `createWorkflowNode` function below. Then reactivity will take care of
// the rest.
const reducer = (acc, workflow) => addNodeToTree(createWorkflowNode(workflow, /* hierarchy */true), acc)
return Object.values(this.workflows)
.reduce(reducer, [])
},
...mapState('gscan', ['gscan']),
// workflowNodes () {
// // NOTE: In case we decide to allow the user to switch between hierarchical and flat
// // gscan view, then all we need to do is just pass a boolean data-property to
// // the `createWorkflowNode` function below. Then reactivity will take care of
// // the rest.
// const reducer = (acc, workflow) => addNodeToTree(createWorkflowNode(workflow, /* hierarchy */true), acc)
// return Object.values(this.workflows)
// .reduce(reducer, [])
// },
/**
* @return {Array<String>}
*/
Expand Down Expand Up @@ -351,7 +323,7 @@ export default {
deep: true,
immediate: false,
handler: function (newVal) {
this.filteredWorkflows = this.filterHierarchically(this.workflowNodes, this.searchWorkflows, this.workflowStates, this.taskStates)
this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, this.searchWorkflows, this.workflowStates, this.taskStates)
}
},
/**
Expand All @@ -361,13 +333,14 @@ export default {
searchWorkflows: {
immediate: false,
handler: function (newVal) {
this.filteredWorkflows = this.filterHierarchically(this.workflowNodes, newVal, this.workflowStates, this.taskStates)
this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, newVal, this.workflowStates, this.taskStates)
}
},
workflowNodes: {
gscan: {
immediate: true,
deep: true,
handler: function () {
this.filteredWorkflows = this.filterHierarchically(this.workflowNodes, this.searchWorkflows, this.workflowStates, this.taskStates)
this.filteredWorkflows = this.filterHierarchically(this.gscan.tree, this.searchWorkflows, this.workflowStates, this.taskStates)
}
}
},
Expand Down Expand Up @@ -415,42 +388,6 @@ export default {
return `/workflows/${ node.node.name }`
}
return ''
},

/**
* Get number of tasks we have in a given state. The states are retrieved
* from `latestStateTasks`, and the number of tasks in each state is from
* the `stateTotals`. (`latestStateTasks` includes old tasks).
*
* @param {WorkflowGraphQLData} workflow - the workflow object retrieved from GraphQL
* @param {string} state - a workflow state
* @returns {number|*} - the number of tasks in the given state
*/
countTasksInState (workflow, state) {
if (Object.hasOwnProperty.call(workflow.stateTotals, state)) {
return workflow.stateTotals[state]
}
return 0
},

getTaskStateClasses (workflow, state) {
const tasksInState = this.countTasksInState(workflow, state)
return tasksInState === 0 ? ['empty-state'] : []
},

// TODO: temporary filter, remove after b0 - https://github.com/cylc/cylc-ui/pull/617#issuecomment-805343847
getLatestStateTasks (latestStateTasks) {
// Values found in: https://github.com/cylc/cylc-flow/blob/9c542f9f3082d3c3d9839cf4330c41cfb2738ba1/cylc/flow/data_store_mgr.py#L143-L149
const validValues = [
TaskState.SUBMITTED.name,
TaskState.SUBMIT_FAILED.name,
TaskState.RUNNING.name,
TaskState.SUCCEEDED.name,
TaskState.FAILED.name
]
return latestStateTasks.filter(entry => {
return validValues.includes(entry[0])
})
}
}
}
Expand Down
133 changes: 133 additions & 0 deletions src/components/cylc/gscan/WorkflowStateSummary.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<!--
Copyright (C) NIWA & British Crown (Met Office) & Contributors.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->

<template>
<!-- task summary tooltips -->
<span>
<span
v-for="[state, tasks] in validLatestStateTasks"
:key="`${nodeId}-summary-${state}`"
:class="getTaskStateClasses(state)"
>
<v-tooltip color="black" top>
<template v-slot:activator="{ on }">
<!-- a v-tooltip does not work directly set on Cylc job component, so we use a dummy button to wrap it -->
<!-- NB: most of the classes/directives in these button are applied so that the user does not notice it is a button -->
<v-btn
v-on="on"
class="ma-0 pa-0"
min-width="0"
min-height="0"
style="font-size: 120%; width: auto"
:ripple="false"
dark
icon
>
<job :status="state" />
</v-btn>
</template>
<!-- tooltip text -->
<span>
<span class="grey--text">{{ countTasksInState(state) }} {{ state }}. Recent {{ state }} tasks:</span>
<br/>
<span v-for="(task, index) in tasks.slice(0, maximumTasksDisplayed)" :key="index">
{{ task }}<br v-if="index !== tasks.length -1" />
</span>
</span>
</v-tooltip>
</span>
</span>
</template>

<script>
import Job from '@/components/cylc/Job'
import TaskState from '@/model/TaskState.model'

/**
* Valid states for a latestStateTasks. See computed variable validLatestStateTasks.
*/
const VALID_STATES = Object.freeze([
TaskState.SUBMITTED.name,
TaskState.SUBMIT_FAILED.name,
TaskState.RUNNING.name,
TaskState.SUCCEEDED.name,
TaskState.FAILED.name
])

export default {
name: 'WorkflowStateSummary',
props: {
nodeId: {
type: String,
required: true
},
/**
* @type {Object}
*/
latestStateTasks: {
type: Object,
required: true
},
stateTotals: {
type: Object,
required: true
},
maximumTasksDisplayed: {
type: Number,
default: 5
}
},
components: {
Job
},
computed: {
// TODO: temporary filter, remove after b0 - https://github.com/cylc/cylc-ui/pull/617#issuecomment-805343847
/**
* @return {Object}
*/
validLatestStateTasks () {
// Values found in: https://github.com/cylc/cylc-flow/blob/9c542f9f3082d3c3d9839cf4330c41cfb2738ba1/cylc/flow/data_store_mgr.py#L143-L149
return Object.entries(this.latestStateTasks).filter(entry => {
return VALID_STATES.includes(entry[0])
})
}
},
methods: {
/**
* Get number of tasks we have in a given state. The states are retrieved
* from `latestStateTasks`, and the number of tasks in each state is from
* the `stateTotals`. (`latestStateTasks` includes old tasks).
*
* @param {String} state - a workflow state
* @returns {Number} - the number of tasks in the given state
*/
countTasksInState (state) {
return this.stateTotals[state] || 0
},
/**
* Defines the CSS class for a state. Useful for handling empty states, when
* we need to compensate for no children HTML elements.
*
* @param {String} state - a workflow state
* @return {Array<String>} - a list of CSS classes (can be empty).
*/
getTaskStateClasses (state) {
return this.countTasksInState(state) === 0 ? ['empty-state'] : []
}
}
}
</script>
25 changes: 15 additions & 10 deletions src/components/cylc/gscan/callbacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
applyDeltasPruned
} from '@/components/cylc/gscan/deltas'
import DeltasCallback from '@/services/callbacks'
import { clear } from '@/components/cylc/gscan/index'

/**
* Provisional GScan callback until https://github.com/cylc/cylc-ui/pull/736
Expand All @@ -29,37 +30,41 @@ import DeltasCallback from '@/services/callbacks'
class GScanCallback extends DeltasCallback {
constructor () {
super()
this.workflows = null
this.lookup = null
this.gscan = null
}

before (deltas, store, errors) {
this.workflows = Object.assign({}, store.state.workflows.workflows)
// If it were TS, we would use a ReadOnly type here...
this.lookup = store.state.workflows.lookup
const gscan = store.state.gscan.gscan
this.gscan = Object.assign({}, gscan)
}

tearDown (store, errors) {
store.commit('workflows/SET_WORKFLOWS', {})
this.workflows = null
clear(this.gscan)
store.commit('gscan/SET_GSCAN', this.gscan)
this.lookup = null
this.gscan = null
}

onAdded (added, store, errors) {
this.workflows = Object.assign(this.workflows, store.state.workflows.workflows)
const results = applyDeltasAdded(added, this.workflows)
const results = applyDeltasAdded(added, this.gscan, {})
errors.push(...results.errors)
}

onUpdated (updated, store, errors) {
const results = applyDeltasUpdated(updated, this.workflows)
const results = applyDeltasUpdated(updated, this.gscan, {})
errors.push(...results.errors)
}

onPruned (pruned, store, errors) {
this.workflows = Object.assign(this.workflows, store.state.workflows.workflows)
const results = applyDeltasPruned(pruned, this.workflows)
const results = applyDeltasPruned(pruned, this.gscan, {})
errors.push(...results.errors)
}

commit (store, errors) {
store.commit('workflows/SET_WORKFLOWS', this.workflows)
store.commit('gscan/SET_GSCAN', this.gscan)
}
}

Expand Down
Loading