Skip to content

Commit 6811744

Browse files
committed
AOTF: fix mutation response handling
1 parent 918a943 commit 6811744

File tree

6 files changed

+207
-55
lines changed

6 files changed

+207
-55
lines changed

changes.d/2174.fix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed spurious error when running a command on multiple workflows.

src/components/core/Alert.vue

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -27,40 +27,35 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
2727
>
2828
<template v-slot:actions>
2929
<v-btn
30-
icon
30+
:icon="mdiClose"
3131
@click="closeAlert"
3232
data-cy="snack-close"
33-
>
34-
<v-icon>{{ $options.icons.mdiClose }}</v-icon>
35-
</v-btn>
33+
/>
3634
</template>
37-
{{ alert.text }}
35+
<p>
36+
{{ alert.text }}
37+
</p>
38+
<p
39+
v-if="alert.detail"
40+
class="mt-2 opacity-80"
41+
>
42+
{{ alert.detail }}
43+
</p>
3844
</v-snackbar>
3945
</template>
4046

41-
<script>
47+
<script setup>
48+
import { computed } from 'vue'
49+
import { useStore } from 'vuex'
4250
import { mdiClose } from '@mdi/js'
43-
import { mapActions, mapState } from 'vuex'
4451
45-
export default {
46-
name: 'Alert',
52+
const store = useStore()
53+
const alert = computed(() => store.state.alert)
4754
48-
computed: {
49-
...mapState(['alert'])
50-
},
51-
52-
methods: {
53-
...mapActions(['setAlert']),
54-
/**
55-
* Dismisses the alert from the UI, also removing it from the Vuex store.
56-
*/
57-
closeAlert () {
58-
this.setAlert(null)
59-
}
60-
},
61-
62-
icons: {
63-
mdiClose
64-
}
55+
/**
56+
* Dismisses the alert from the UI, also removing it from the Vuex store.
57+
*/
58+
function closeAlert () {
59+
store.dispatch('setAlert', null)
6560
}
6661
</script>

src/model/Alert.model.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ export class Alert {
2121
* @param {Error|string} err - original thrown error to log if possible, or an error message.
2222
* @param {string} color - color of the displayed alert.
2323
* @param {?string} msg - a custom message to display in the alert instead of err.
24+
* @param {?string} detail - more details to show.
2425
*/
25-
constructor (err, color, msg = null) {
26+
constructor (err, color, msg = null, detail = null) {
2627
this.err = err
2728
this.text = msg || err
2829
this.color = color
30+
this.detail = detail
2931
}
3032
}

src/utils/aotf.js

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { Alert } from '@/model/Alert.model'
5252
import { store } from '@/store/index'
5353
import { Tokens } from '@/utils/uid'
5454
import { WorkflowState, WorkflowStateNames } from '@/model/WorkflowState.model'
55+
import { isBoolean } from 'lodash-es'
5556

5657
/** @typedef {import('@apollo/client').ApolloClient} ApolloClient */
5758
/** @typedef {import('graphql').IntrospectionInputType} IntrospectionInputType */
@@ -911,10 +912,16 @@ async function _mutateError (mutationName, err, response) {
911912
console.error('mutation response', response)
912913
}
913914

915+
let detail = ''
916+
for (const e of err.cause?.result?.errors ?? []) {
917+
console.error(e)
918+
detail += `${e.message}\n`
919+
}
920+
914921
// open a user alert
915922
await store.dispatch(
916923
'setAlert',
917-
new Alert(err, 'error', `Command failed: ${mutationName} - ${err}`)
924+
new Alert(err, 'error', `Command failed: ${mutationName} - ${err}`, detail)
918925
)
919926

920927
// format a response
@@ -936,14 +943,10 @@ async function _mutateError (mutationName, err, response) {
936943
*/
937944
export async function mutate (mutation, variables, apolloClient, cylcID) {
938945
const mutationStr = constructMutation(mutation)
939-
let response = null
940946
// eslint-disable-next-line no-console
941-
console.debug([
942-
`mutation(${mutation.name})`,
943-
mutationStr,
944-
variables
945-
])
947+
console.debug(mutationStr, variables)
946948

949+
let response
947950
try {
948951
// call the mutation
949952
response = await apolloClient.mutate({
@@ -961,23 +964,36 @@ export async function mutate (mutation, variables, apolloClient, cylcID) {
961964
}
962965

963966
try {
964-
const { result } = response.data[mutation.name]
965-
if (Array.isArray(result) && result.length === 2) {
966-
// regular [commandSucceeded, message] format
967-
if (result[0] === true) {
968-
// success
969-
return _mutateSuccess(result[1])
970-
}
971-
// failure (Cylc error, e.g. could not find workflow <x>)
972-
return _mutateError(mutation.name, result[1], response)
973-
}
974-
// command in a different format (e.g. info command)
975-
return _mutateSuccess(result)
967+
return handleMutationResponse(mutation, response)
976968
} catch (error) {
977969
return _mutateError(mutation.name, 'invalid response', response)
978970
}
979971
}
980972

973+
export function handleMutationResponse (mutation, response) {
974+
const { result } = response.data[mutation.name]
975+
if (Array.isArray(result)) {
976+
if (result.length === 2 && isBoolean(result[0])) {
977+
// Expected response format
978+
const [success, msg] = result
979+
if (success) return _mutateSuccess(msg)
980+
// failure (Cylc error, e.g. could not find workflow <x>)
981+
return _mutateError(mutation.name, msg, response)
982+
}
983+
// Handle possible nested response formats (https://github.com/cylc/cylc-ui/issues/1851):
984+
const results = result.map(
985+
(r) => r[mutation.name]?.result?.[0]?.response ?? r.response
986+
)
987+
for (const [success, msg] of results) {
988+
if (!success) return _mutateError(mutation.name, msg, response)
989+
}
990+
return _mutateSuccess('Command(s) submitted')
991+
}
992+
console.warn('Unexpected format for mutation response:', result)
993+
// But assume success
994+
return _mutateSuccess(result)
995+
}
996+
981997
/**
982998
* Send a GraphQL query.
983999
*

tests/unit/services/workflow.service.spec.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,15 @@ import WorkflowService from '@/services/workflow.service'
3030
import ViewState from '@/model/ViewState.model'
3131
import { TreeCallback, WorkflowCallback } from './testCallback'
3232

33+
vi.mock('@/graphql/index', () => ({
34+
createApolloClient: () => ({
35+
query: vi.fn(),
36+
subscribe: () => ({
37+
subscribe: vi.fn()
38+
}),
39+
})
40+
}))
41+
3342
const sandbox = sinon.createSandbox()
3443

3544
describe('WorkflowService', () => {
@@ -63,14 +72,6 @@ describe('WorkflowService', () => {
6372
let subscription
6473
beforeEach(() => {
6574
sandbox.stub(console, 'debug')
66-
vi.mock('@/graphql/index', () => ({
67-
createApolloClient: () => ({
68-
query: vi.fn(),
69-
subscribe: () => ({
70-
subscribe: vi.fn()
71-
}),
72-
})
73-
}))
7475
subscriptionClient = null
7576
// TODO: really load some mutations
7677
sandbox.stub(WorkflowService.prototype, 'loadTypes').returns(

tests/unit/utils/aotf.spec.js

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/**
1+
/*
22
* Copyright (C) NIWA & British Crown (Met Office) & Contributors.
33
*
44
* This program is free software: you can redistribute it and/or modify
@@ -15,10 +15,12 @@
1515
* along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
*/
1717

18+
import { afterAll, beforeAll } from 'vitest'
1819
import * as aotf from '@/utils/aotf'
1920
import dedent from 'dedent'
2021
// need the polyfill as otherwise ApolloClient fails to be imported as it checks for a global fetch object on import...
2122
import 'cross-fetch/polyfill'
23+
import sinon from 'sinon'
2224

2325
describe('aotf (Api On The Fly)', () => {
2426
describe('tokenise', () => {
@@ -751,4 +753,139 @@ describe('aotf (Api On The Fly)', () => {
751753
])
752754
})
753755
})
756+
757+
describe('handleMutationResponse()', () => {
758+
const mutation = { name: 'breach' }
759+
beforeAll(() => {
760+
sinon.stub(console, 'error')
761+
sinon.stub(console, 'warn')
762+
})
763+
afterAll(() => {
764+
sinon.restore()
765+
})
766+
767+
it.each([
768+
{
769+
testID: 'original interface',
770+
response: {
771+
data: {
772+
[mutation.name]: {
773+
result: [true, 'arasaka']
774+
}
775+
}
776+
},
777+
expected: {
778+
status: 'SUCCEEDED', message: 'arasaka'
779+
}
780+
},
781+
782+
{
783+
testID: 'original interface with failure',
784+
response: {
785+
data: {
786+
[mutation.name]: {
787+
result: [false, 'relic malfunction']
788+
}
789+
}
790+
},
791+
expected: {
792+
status: 'FAILED', message: 'relic malfunction'
793+
}
794+
},
795+
796+
{
797+
testID: 'nested',
798+
response: {
799+
data: {
800+
[mutation.name]: {
801+
result: [
802+
{
803+
[mutation.name]: {
804+
result: [{ id: 'wflow1', response: [true, 'arasaka'] }]
805+
}
806+
},
807+
{
808+
[mutation.name]: {
809+
result: [{ id: 'wflow2', response: [true, 'kiroshi'] }]
810+
}
811+
}
812+
]
813+
}
814+
}
815+
},
816+
expected: {
817+
status: 'SUCCEEDED', message: 'Command(s) submitted'
818+
}
819+
},
820+
821+
{
822+
testID: 'nested with failure',
823+
response: {
824+
data: {
825+
[mutation.name]: {
826+
result: [
827+
{
828+
[mutation.name]: {
829+
result: [{ id: 'wflow1', response: [true, 'arasaka'] }]
830+
}
831+
},
832+
{
833+
[mutation.name]: {
834+
result: [{ id: 'wflow2', response: [false, 'relic malfunction'] }]
835+
}
836+
}
837+
]
838+
}
839+
}
840+
},
841+
expected: {
842+
status: 'FAILED', message: 'relic malfunction'
843+
}
844+
},
845+
846+
{
847+
testID: 'other format',
848+
response: {
849+
data: {
850+
[mutation.name]: {
851+
result: [{ response: [true, 'arasaka'] }]
852+
}
853+
}
854+
},
855+
expected: {
856+
status: 'SUCCEEDED', message: 'Command(s) submitted'
857+
}
858+
},
859+
860+
{
861+
testID: 'other format with failure',
862+
response: {
863+
data: {
864+
[mutation.name]: {
865+
result: [{ response: [false, 'relic malfunction'] }]
866+
}
867+
}
868+
},
869+
expected: {
870+
status: 'FAILED', message: 'relic malfunction'
871+
}
872+
},
873+
874+
{
875+
testID: 'unexpected format - assume success',
876+
response: {
877+
data: {
878+
[mutation.name]: {
879+
result: 2077,
880+
}
881+
}
882+
},
883+
expected: {
884+
status: 'SUCCEEDED', message: 2077
885+
}
886+
}
887+
])('handles response - $testID', async ({ response, expected, testID }) => {
888+
expect(await aotf.handleMutationResponse(mutation, response)).toStrictEqual(expected)
889+
})
890+
})
754891
})

0 commit comments

Comments
 (0)