diff --git a/src/components/cylc/cylcObject/Menu.vue b/src/components/cylc/cylcObject/Menu.vue index 72a5cde8c..6643fe406 100644 --- a/src/components/cylc/cylcObject/Menu.vue +++ b/src/components/cylc/cylcObject/Menu.vue @@ -59,12 +59,12 @@ along with this program. If not, see . - + {{ mutation._icon }} @@ -136,7 +136,8 @@ import Mutation from '@/components/cylc/Mutation' import { mdiPencil } from '@mdi/js' -import { mapState } from 'vuex' +import { mapGetters, mapState } from 'vuex' +import WorkflowState from '@/model/WorkflowState.model' export default { name: 'CylcObjectMenu', @@ -160,6 +161,7 @@ export default { dialogKey: false, expanded: false, node: null, + workflowStatus: null, mutations: [], isLoadingMutations: true, showMenu: false, @@ -181,6 +183,7 @@ export default { }, computed: { + ...mapGetters('workflows', ['getNodes']), primaryMutations () { return this.$workflowService.primaryMutations[this.node.type] || [] }, @@ -194,11 +197,15 @@ export default { } const shortList = this.primaryMutations if (!this.expanded && shortList.length) { - return this.mutations.filter( - x => shortList.includes(x.mutation.name) - ).sort( - (x, y) => shortList.indexOf(x.mutation.name) - shortList.indexOf(y.mutation.name) - ) + return this.mutations + // filter for shortlisted mutations + .filter(x => shortList.includes(x.mutation.name)) + // filter out mutations which aren't relevant to the workflow state + .filter(x => !this.isDisabled(x.mutation, true)) + // sort by definition order + .sort( + (x, y) => shortList.indexOf(x.mutation.name) - shortList.indexOf(y.mutation.name) + ) } return this.mutations }, @@ -232,12 +239,29 @@ export default { methods: { isEditable (authorised, mutation) { - if (!authorised || mutation.name === 'log') { + if (mutation.name === 'log' || this.isDisabled(mutation, authorised)) { return true } else { return false } }, + isDisabled (mutation, authorised) { + if (this.node.type !== 'workflow') { + const nodeReturned = this.getNodes( + 'workflow', [this.node.tokens.workflow_id]) + if (nodeReturned.length) { + this.workflowStatus = nodeReturned[0].node.status + } else { this.workflowStatus = WorkflowState.RUNNING.name } + } else { + this.workflowStatus = this.node.node.status + } + if ( + (!mutation._validStates.includes(this.workflowStatus)) || + !authorised) { + return true + } + return false + }, openDialog (mutation) { if (mutation.name === 'log') { this.showMenu = false diff --git a/src/services/mock/json/IntrospectionQuery.json b/src/services/mock/json/IntrospectionQuery.json index 629b2a97a..ba84f991a 100644 --- a/src/services/mock/json/IntrospectionQuery.json +++ b/src/services/mock/json/IntrospectionQuery.json @@ -1427,7 +1427,7 @@ "fields": [ { "name": "broadcast", - "description": "Override `[runtime]` configurations in a running workflow.\n\nUses for broadcast include making temporary changes to task\nbehaviour, and task-to-downstream-task communication via\nenvironment variables.\n\nA broadcast can set/override any `[runtime]` configuration for all\ncycles or for a specific cycle. If a task is affected by\nspecific-cycle and all-cycle broadcasts at the same time, the\nspecific takes precedence.\n\nBroadcasts can also target all tasks, specific tasks or families of\ntasks. If a task is affected by broadcasts to multiple ancestor\nnamespaces (tasks it inherits from), the result is determined by\nnormal `[runtime]` inheritance.\n\nBroadcasts are applied at the time of job submission.\n\nBroadcasts persist, even across restarts. Broadcasts made to\nspecific cycle points will expire when the cycle point is older\nthan the oldest active cycle point in the workflow.\n\nActive broadcasts can be revoked using the \"clear\" mode.\nAny broadcasts matching the specified cycle points and\nnamespaces will be revoked.\n\nNote: a \"clear\" broadcast for a specific cycle or namespace does\n*not* clear all-cycle or all-namespace broadcasts.", + "description": "Override `[runtime]` configurations in a running workflow.\n\nUses for broadcast include making temporary changes to task\nbehaviour, and task-to-downstream-task communication via\nenvironment variables.\n\nA broadcast can set/override any `[runtime]` configuration for all\ncycles or for a specific cycle. If a task is affected by\nspecific-cycle and all-cycle broadcasts at the same time, the\nspecific takes precedence.\n\nBroadcasts can also target all tasks, specific tasks or families of\ntasks. If a task is affected by broadcasts to multiple ancestor\nnamespaces (tasks it inherits from), the result is determined by\nnormal `[runtime]` inheritance.\n\nBroadcasts are applied at the time of job submission.\n\nBroadcasts persist, even across restarts. Broadcasts made to\nspecific cycle points will expire when the cycle point is older\nthan the oldest active cycle point in the workflow.\n\nActive broadcasts can be revoked using the \"clear\" mode.\nAny broadcasts matching the specified cycle points and\nnamespaces will be revoked.\n\nNote: a \"clear\" broadcast for a specific cycle or namespace does\n*not* clear all-cycle or all-namespace broadcasts.\n Valid for: paused, running workflows.", "args": [ { "name": "cutoff", diff --git a/src/utils/aotf.js b/src/utils/aotf.js index 27becc72a..4e18e5806 100644 --- a/src/utils/aotf.js +++ b/src/utils/aotf.js @@ -50,6 +50,7 @@ import { import Alert from '@/model/Alert.model' import store from '@/store/index' import { Tokens } from '@/utils/uid' +import { WorkflowState } from '@/model/WorkflowState.model' // Typedef imports /* eslint-disable no-unused-vars, no-duplicate-imports */ @@ -167,6 +168,7 @@ export const cylcObjects = Object.freeze({ export const primaryMutations = { [cylcObjects.Workflow]: [ 'play', + 'resume', 'pause', 'stop', 'reload', @@ -432,9 +434,35 @@ export function processMutations (mutations, types) { mutation._icon = mutationIcons[mutation.name] || mutationIcons[''] mutation._shortDescription = getMutationShortDesc(mutation.description) mutation._help = getMutationExtendedDesc(mutation.description) + mutation._validStates = getStates(mutation.description) processArguments(mutation, types) } } +/** + * Get the workflow states that the mutation is valid for. + * + * @export + * @param {string=} text - Full mutation description. + * @return {Array} + */ +export function getStates (text) { + const defaultStates = [ + WorkflowState.RUNNING.name, + WorkflowState.PAUSED.name, + WorkflowState.STOPPING.name, + WorkflowState.STOPPED.name + ] + if (!text) { + return defaultStates + } + const re = /Valid\sfor:\s(.*)\sworkflows./ + // default to all workflow states + const validStates = text.match(re) + if (validStates) { + return validStates[1].replace(/\s/g, '').split(',') + } + return defaultStates +} /** * Get the first part of a mutation description (up to the first double newline). diff --git a/tests/e2e/specs/menu.cy.js b/tests/e2e/specs/menu.cy.js index df392f7ab..f2fdd90e7 100644 --- a/tests/e2e/specs/menu.cy.js +++ b/tests/e2e/specs/menu.cy.js @@ -16,7 +16,7 @@ */ describe('CylcObject Menu component', () => { - const collapsedWorkflowMenuLength = 6 // (5 mutations + "show more" btn) + const collapsedWorkflowMenuLength = 7 // (6 mutations + "show more" btn) const expandedWorkflowMenuLength = 21 beforeEach(() => { diff --git a/tests/e2e/support/graphql.js b/tests/e2e/support/graphql.js index af38358c9..2a627bf02 100644 --- a/tests/e2e/support/graphql.js +++ b/tests/e2e/support/graphql.js @@ -27,6 +27,7 @@ const MUTATIONS = [ in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + Valid for: running workflows. `, args: [ { @@ -43,6 +44,7 @@ const MUTATIONS = [ _title: 'Unauthorised Mutation', description: ` A mutation user will not be authorised for. + Valid for: running workflows. `, args: [ { diff --git a/tests/unit/components/cylc/tree/tree.data.js b/tests/unit/components/cylc/tree/tree.data.js index 321048c9b..2b8f04814 100644 --- a/tests/unit/components/cylc/tree/tree.data.js +++ b/tests/unit/components/cylc/tree/tree.data.js @@ -26,7 +26,10 @@ const simpleWorkflowTree4Nodes = [ type: 'workflow', node: { __typename: 'Workflow', - state: 'running' + state: 'running', + node: { + status: 'running' + } }, children: [ { diff --git a/tests/unit/utils/aotf.spec.js b/tests/unit/utils/aotf.spec.js index ace265811..767af4fe8 100644 --- a/tests/unit/utils/aotf.spec.js +++ b/tests/unit/utils/aotf.spec.js @@ -59,11 +59,17 @@ describe('aotf (Api On The Fly)', () => { }) }) + describe('getStates', () => { + it('gets valid states', () => { + expect(aotf.getStates('Valid for: running, stopped workflows.')).to.deep.equal(['running', 'stopped']) + }) + }) + describe('processMutations', () => { it('should add computed fields', () => { const input = { name: 'fooBar', - description: 'Short description.\n\nLong\ndescription.', + description: 'Short description.\n\nLong\ndescription.\nValid for: stopped, paused workflows.', args: [] } const output = { @@ -71,7 +77,8 @@ describe('aotf (Api On The Fly)', () => { _title: 'Foo Bar', _icon: aotf.mutationIcons[''], _shortDescription: 'Short description.', - _help: 'Long\ndescription.' + _help: 'Long\ndescription.\nValid for: stopped, paused workflows.', + _validStates: ['stopped', 'paused'] } aotf.processMutations([input], null) expect(input).to.deep.equal(output)