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

GScan improvements, propagate state #736

Closed
wants to merge 16 commits into from
Closed
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
135 changes: 135 additions & 0 deletions src/components/cylc/common/deltas.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Vue from 'vue'
import mergeWith from 'lodash/mergeWith'
import isArray from 'lodash/isArray'
import { mergeWithCustomizer } from '@/components/cylc/common/merge'

/**
* @typedef {Object} GraphQLResponseData
* @property {Deltas} deltas
Expand Down Expand Up @@ -79,3 +84,133 @@
* @property {Array<string>} familyProxies - IDs of family proxies removed
* @property {Array<string>} jobs - IDs of jobs removed
*/

/**
* @typedef {Object} Result
* @property {Array<Object>} errors
*/

/**
* @param {DeltasAdded} added
* @param {Object.<String, Object>} lookup
* @return {Result}
*/
function applyDeltasAdded (added, lookup) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code added here was in workflows/deltas.js, but was used only by the Tree view. Instead of moving to the tree view, I've moved it here to common since if we ever need to use a delta that creates a lookup, anyone can simply call this function ☝️

I think I won't need a lookup for GScan, at least for now. Or at least I am avoiding adding one unless really needed. But if I need to add one, then I'll use this function.

const result = {
errors: []
}
Object.values(added).forEach(addedValue => {
const items = isArray(addedValue) ? addedValue : [addedValue]
items.forEach(addedData => {
// An example for a data without .id, is the empty delta with __typename: "Added". It does occur, and can cause runtime errors.
if (addedData.id) {
try {
Vue.set(lookup, addedData.id, addedData)
} catch (error) {
result.errors.push([
'Error applying added-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state',
error,
addedData,
lookup
])
}
}
})
})
return result
}

/**
* Deltas updated.
*
* @param updated {DeltasUpdated} updated
* @param {Object.<String, Object>} lookup
* @return {Result}
*/
function applyDeltasUpdated (updated, lookup) {
const result = {
errors: []
}
Object.values(updated).forEach(updatedValue => {
const items = isArray(updatedValue) ? updatedValue : [updatedValue]
items.forEach(updatedData => {
// An example for a data without .id, is the empty delta with __typename: "Updated". It does occur, and can cause runtime errors.
if (updatedData.id) {
try {
const existingNode = lookup[updatedData.id]
if (existingNode) {
mergeWith(existingNode, updatedData, mergeWithCustomizer)
} else {
// TODO: we are adding in the updated. Is that OK? Should we revisit it later perhaps?
Vue.set(lookup, updatedData.id, updatedData)
}
} catch (error) {
result.errors.push([
'Error applying updated-delta, see browser console logs for more. Please reload your browser tab to retrieve the full flow state',
error,
updatedData,
lookup
])
}
}
})
})
return result
}

/**
* Deltas pruned.
*
* @param {DeltasPruned} pruned - deltas pruned
* @param {Object.<String, Object>} lookup
* @return {Result}
*/
function applyDeltasPruned (pruned, lookup) {
Object.values(pruned).forEach(prunedData => {
// It can be a String, when you get a delta with only '{ __typename: "Pruned" }'
const items = isArray(prunedData) ? prunedData : [prunedData]
for (const id of items) {
if (lookup[id]) {
delete lookup[id]
}
}
})
return {
errors: []
}
}

/**
* A function that simply applies the deltas to a lookup object.
*
* The entries in deltas will be the value of the lookup, and their ID's
* will be the keys.
*
* This function can be used with any lookup-like structure. When
* entries are updated it will merge with a customizer maintaining
* the Vue reactivity.
*
* @param {GraphQLResponseData} data
* @param {Object.<String, Object>} lookup
*/
export default function (data, lookup) {
const added = data.deltas.added
const updated = data.deltas.updated
const pruned = data.deltas.pruned
const errors = []
if (added) {
const result = applyDeltasAdded(added, lookup)
errors.push(...result.errors)
}
if (updated) {
const result = applyDeltasUpdated(updated, lookup)
errors.push(...result.errors)
}
if (pruned) {
const result = applyDeltasPruned(pruned, lookup)
errors.push(...result.errors)
}
return {
errors
}
}
117 changes: 28 additions & 89 deletions src/components/cylc/gscan/GScan.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,42 +142,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 @@ -203,19 +175,19 @@ 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 { GSCAN_DELTAS_SUBSCRIPTION } from '@/graphql/queries'

export default {
name: 'GScan',
components: {
Job,
Tree,
WorkflowIcon
WorkflowIcon,
WorkflowStateSummary
},
mixins: [
subscriptionComponentMixin
Expand All @@ -226,7 +198,9 @@ export default {
GSCAN_DELTAS_SUBSCRIPTION,
{},
'root',
['workflows/applyWorkflowsDeltas'],
[
'workflows/applyGScanDeltas'
],
['workflows/clearWorkflows']
),
maximumTasksDisplayed: 5,
Expand Down Expand Up @@ -310,16 +284,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('workflows', ['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 @@ -348,7 +322,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 @@ -358,13 +332,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 @@ -412,42 +387,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
Loading