Skip to content

Commit affdd8a

Browse files
Merge pull request cylc#1786 from MetRonnie/n-win
Fix N-window selector sending mutation upon navigation to stopped workflows
2 parents 41dc290 + fe8b762 commit affdd8a

File tree

4 files changed

+135
-27
lines changed

4 files changed

+135
-27
lines changed

src/components/cylc/workflow/Toolbar.vue

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -89,21 +89,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
8989
:disabled="isStopped"
9090
link
9191
size="small"
92+
data-cy="n-win-selector"
9293
>
9394
N={{ nWindow }}
94-
<v-menu activator="parent" :close-on-content-click="false">
95+
<v-menu
96+
activator="parent"
97+
:close-on-content-click="false"
98+
max-width="400"
99+
data-cy="n-win-popup"
100+
>
95101
<v-card title="Graph Window Depth">
96-
<v-card-subtitle>
102+
<v-card-text>
97103
This changes the number of tasks which are displayed.
98104

99105
Higher values may impact performance.
100-
</v-card-subtitle>
106+
</v-card-text>
101107
<v-card-text>
102108
<v-select
103-
density="compact"
104109
v-model="nWindow"
105110
:items="[0,1,2,3]"
106-
/>
111+
>
112+
<template #append-inner>
113+
<v-progress-circular
114+
v-if="changingNWindow"
115+
indeterminate
116+
size="20"
117+
width="2"
118+
/>
119+
</template>
120+
</v-select>
107121
</v-card-text>
108122
</v-card>
109123
</v-menu>
@@ -198,6 +212,7 @@ import {
198212
mdiAccount
199213
} from '@mdi/js'
200214
import { startCase } from 'lodash'
215+
import { until } from '@/utils'
201216
import { useToolbar, toolbarHeight } from '@/utils/toolbar'
202217
import WorkflowState from '@/model/WorkflowState.model'
203218
import graphql from '@/mixins/graphql'
@@ -279,7 +294,7 @@ export default {
279294
paused: null,
280295
stop: null
281296
},
282-
nWindow: 1
297+
changingNWindow: false,
283298
}),
284299
285300
computed: {
@@ -357,9 +372,20 @@ export default {
357372
)
358373
}
359374
},
360-
nEdgeDistance () {
361-
// the graph window distance reported by the scheduler
362-
return this.currentWorkflow?.node?.nEdgeDistance
375+
nWindow: {
376+
get () {
377+
// the graph window distance reported by the scheduler
378+
return this.currentWorkflow?.node?.nEdgeDistance ?? 1
379+
},
380+
async set (val) {
381+
if (val == null || this.isStopped) return
382+
383+
this.changingNWindow = true
384+
if (await this.setGraphWindow(val)) {
385+
await until(() => this.currentWorkflow?.node?.nEdgeDistance === val)
386+
}
387+
this.changingNWindow = false
388+
},
363389
}
364390
},
365391
@@ -373,16 +399,6 @@ export default {
373399
isStopped () {
374400
this.expecting.stop = null
375401
},
376-
nWindow (newVal) {
377-
// the user has requested to change the window size
378-
this.setGraphWindow(newVal)
379-
},
380-
nEdgeDistance (newVal) {
381-
// the scheduler has reported that the window size has changed
382-
if (newVal !== undefined) {
383-
this.nWindow = newVal
384-
}
385-
}
386402
},
387403
388404
methods: {
@@ -416,12 +432,13 @@ export default {
416432
}
417433
})
418434
},
419-
setGraphWindow (nWindow) {
420-
this.$workflowService.mutate(
435+
async setGraphWindow (nWindow) {
436+
const { status } = await this.$workflowService.mutate(
421437
'setGraphWindowExtent',
422438
this.currentWorkflow.id,
423439
{ nEdgeDistance: nWindow }
424440
)
441+
return status === mutationStatus.SUCCEEDED
425442
},
426443
startCase,
427444
},

src/utils/index.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,30 @@ export const getPageTitle = (key, params = {}) => {
3838
* @param {import('vue').WatchCallback} callback
3939
* @param {import('vue').WatchOptions?} options
4040
*/
41-
export const when = (source, callback, options = {}) => {
41+
export function when (source, callback, options = {}) {
4242
const unwatch = watch(
4343
source,
4444
(ready) => {
4545
if (ready) {
46-
callback()
4746
unwatch()
47+
callback()
4848
}
4949
},
5050
{ immediate: true, ...options }
5151
)
5252
}
53+
54+
/**
55+
* Return a promise that resolves when the source becomes truthy.
56+
*
57+
* Awaitable version of when().
58+
*
59+
* @param {import('vue').WatchSource} source
60+
* @param {import('vue').WatchOptions?} options
61+
* @returns {Promise<void>}
62+
*/
63+
export function until (source, options = {}) {
64+
return new Promise((resolve) => {
65+
when(source, resolve, options)
66+
})
67+
}

tests/e2e/specs/toolbar.cy.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
*/
1717

18+
import { mutationStatus } from '@/utils/aotf'
19+
import { Deferred } from '$tests/util'
20+
1821
describe('Toolbar component', () => {
1922
it('Is displayed when we are looking at a workflow', () => {
2023
cy.visit('/#/workspace/one')
@@ -63,3 +66,68 @@ describe('Toolbar Component authenticated user', () => {
6366
.should('have.text', 'U')
6467
})
6568
})
69+
70+
describe('N-window selector', () => {
71+
/** Capture mutations. */
72+
function captureGraphWinMutation () {
73+
const mutations = []
74+
const deferred = new Deferred()
75+
cy.window().its('app.$workflowService').then((service) => {
76+
// mock the workflow service's mutate method to catch high-level calls
77+
service.mutate = async (name, id, args) => {
78+
switch (name) {
79+
case 'setGraphWindowExtent':
80+
mutations.push(args.nEdgeDistance)
81+
await deferred.promise
82+
return [mutationStatus.SUCCEEDED, '']
83+
default:
84+
throw new Error('This mutation is not mocked by the tests')
85+
}
86+
}
87+
})
88+
return { mutations, deferred }
89+
}
90+
91+
it('Is enabled for workflows that are not stopped', () => {
92+
// Disabled for stopped workflow:
93+
cy.visit('/#/workspace/multi/level/run1')
94+
.get('#core-app-bar')
95+
.find('[data-cy=n-win-selector]')
96+
.should('have.attr', 'disabled')
97+
// Enabled for running workflow:
98+
cy.visit('/#/workspace/one')
99+
.get('[data-cy=n-win-selector]')
100+
.click()
101+
.get('[data-cy=n-win-popup]')
102+
.find('input')
103+
.invoke('val')
104+
.should('eq', '1')
105+
})
106+
107+
it('Sets the n-window', () => {
108+
cy.visit('/#/workspace/one')
109+
const { mutations, deferred } = captureGraphWinMutation()
110+
cy.get('[data-cy=n-win-selector]')
111+
.click()
112+
.get('[data-cy=n-win-popup]')
113+
.find('[role=combobox]')
114+
.click()
115+
.invoke('attr', 'aria-owns').then((dropdownID) => {
116+
cy.get(`#${dropdownID}`)
117+
.contains('3')
118+
.click()
119+
.then(() => {
120+
expect(mutations).to.deep.equal([3])
121+
// Should show loading spinner while waiting for response
122+
cy.get('[data-cy=n-win-popup] .v-progress-circular').as('loadingSpinner')
123+
.should('be.visible').then(() => {
124+
// Allow the "response" to complete
125+
deferred.resolve()
126+
cy.get('@loadingSpinner')
127+
.should('not.exist')
128+
// Note we cannot mock websockets in Cypress so we cannot test the actual update of the n-window in the GraphQL subscription
129+
})
130+
})
131+
})
132+
})
133+
})

tests/unit/utils/index.spec.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,27 @@
1616
*/
1717

1818
import { nextTick, ref } from 'vue'
19-
import { getPageTitle, when } from '@/utils/index'
19+
import { getPageTitle, when, until } from '@/utils/index'
2020

2121
describe('getPageTitle()', () => {
2222
it('displays the application title correctly', () => {
2323
expect(getPageTitle('App.dashboard')).to.equal('Cylc UI | Dashboard')
2424
})
2525
})
2626

27-
describe('when()', () => {
28-
it('watches source until true and then stops watching', async () => {
27+
describe.each([
28+
{ func: when, description: 'watches source until true and then stops watching' },
29+
{ func: until, description: 'returns a promise that resolves when source becomes true' },
30+
])('$func', ({ func, description }) => {
31+
it(description, async () => {
2932
const source = ref(false)
3033
let counter = 0
31-
when(source, () => counter++)
34+
switch (func) {
35+
case when:
36+
when(source, () => counter++); break
37+
case until:
38+
until(source).then(() => counter++); break
39+
}
3240
expect(counter).toEqual(0)
3341
source.value = true
3442
await nextTick()

0 commit comments

Comments
 (0)