Skip to content

Commit 6481204

Browse files
feat: when query variables fail to load, show a message to the user (#3444)
1 parent 5f812d0 commit 6481204

File tree

7 files changed

+88
-10
lines changed

7 files changed

+88
-10
lines changed

cypress/e2e/shared/dashboardsView.test.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -884,9 +884,17 @@ csv.from(csv: data) |> filter(fn: (r) => r.bucket == v.bucketsCSV)`
884884
// and cause the rest to exist in loading states
885885
cy.getByTestID('variable-dropdown--build').should(
886886
'contain',
887-
'Loading'
887+
'Error'
888888
)
889889

890+
// An error notification should tell the user that this failed
891+
cy.getByTestID('notification-error').contains(
892+
'could not find bucket "beans"'
893+
)
894+
cy.getByTestID('notification-error--dismiss').click({
895+
multiple: true,
896+
})
897+
890898
cy.getByTestIDSubStr('cell--view-empty')
891899

892900
// But selecting a nonempty bucket should load some data
@@ -1039,11 +1047,17 @@ csv.from(csv: data) |> filter(fn: (r) => r.bucket == v.bucketsCSV)`
10391047
'beans'
10401048
)
10411049

1042-
// and cause the rest to exist in loading states
1050+
// and cause the rest to exist in error states
10431051
cy.getByTestIDSubStr('variable-dropdown--dependent').should(
10441052
'contain',
1045-
'Loading'
1053+
'Error'
1054+
)
1055+
1056+
// An error notification should tell the user that this failed
1057+
cy.getByTestID('notification-error').contains(
1058+
'could not find bucket "beans"'
10461059
)
1060+
cy.getByTestID('notification-error--dismiss').click()
10471061

10481062
cy.getByTestIDSubStr('cell--view-empty')
10491063

src/shared/copy/notifications.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,12 @@ export const getVariableFailed = (): Notification => ({
428428
message: 'Failed to fetch variable',
429429
})
430430

431+
export const getVariableFailedWithMessage = (name, message): Notification => ({
432+
...defaultErrorNotification,
433+
duration: INDEFINITE,
434+
message: `Failed to fetch variable ${name}: ${message}`,
435+
})
436+
431437
export const createVariableFailed = (error: string): Notification => ({
432438
...defaultErrorNotification,
433439
icon: IconFont.Cube,

src/variables/actions/thunks.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export const getVariables = (controller?: AbortController) => async (
8282
{query: {orgID: org.id}},
8383
{signal: controller?.signal}
8484
)
85+
8586
if (!resp) {
8687
return
8788
}
@@ -186,6 +187,12 @@ export const hydrateVariables = (
186187
dispatch(setVariable(variable.id, RemoteDataState.Done, _variable))
187188
}
188189
})
190+
191+
hydration.on('error', (variable, error) => {
192+
dispatch(setVariable(variable.id, RemoteDataState.Error))
193+
dispatch(notify(copy.getVariableFailedWithMessage(variable?.name, error)))
194+
})
195+
189196
await hydration.promise
190197
}
191198

src/variables/components/TypeAheadVariableDropdown.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,11 @@ class TypeAheadVariableDropdown extends PureComponent<Props, MyState> {
267267
}
268268

269269
const getInnerComponent = () => {
270-
if (status === RemoteDataState.Loading || this.noValuesPresent()) {
270+
if (
271+
status === RemoteDataState.Loading ||
272+
status === RemoteDataState.Error ||
273+
this.noValuesPresent()
274+
) {
271275
return placeHolderText
272276
} else {
273277
return (
@@ -385,6 +389,10 @@ class TypeAheadVariableDropdown extends PureComponent<Props, MyState> {
385389
if (status === RemoteDataState.Loading) {
386390
return 'Loading...'
387391
}
392+
if (status === RemoteDataState.Error) {
393+
return 'Error'
394+
}
395+
388396
if (this.noFilteredValuesPresent()) {
389397
return 'No Values'
390398
}

src/variables/mocks/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ export const defaultVariableAssignments: VariableAssignment[] = [
4646
export const createVariable = (
4747
name: string,
4848
query: string,
49-
selected?: string
49+
selected?: string,
50+
status: RemoteDataState = RemoteDataState.Done
5051
): Variable => ({
5152
name,
5253
id: name,
@@ -60,7 +61,7 @@ export const createVariable = (
6061
language: 'flux',
6162
},
6263
},
63-
status: RemoteDataState.Done,
64+
status,
6465
})
6566

6667
export const createMapVariable = (

src/variables/utils/hydrateVars.test.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,16 @@ describe('hydrate vars', () => {
177177

178178
fetcher.setResponse(b, Promise.reject('oopsy whoopsies'))
179179

180-
const actual = await hydrateVars(vars, vars, {
180+
const response = hydrateVars(vars, vars, {
181181
url: '',
182182
orgID: '',
183183
selections: {},
184184
fetcher,
185-
}).promise
185+
})
186+
187+
const mockFire = jest.spyOn(response.on, 'fire')
188+
189+
const actual = await response.promise
186190

187191
// We expect the following end state:
188192
//
@@ -204,6 +208,12 @@ describe('hydrate vars', () => {
204208
actual.filter(v => v.id === 'c')[0].arguments.values.results
205209
).toEqual(['cVal'])
206210
expect(actual.filter(v => v.id === 'c')[0].selected).toEqual(['cVal'])
211+
212+
const errorCall = mockFire.mock.calls[4]
213+
const [eventName, variable, errorMessage] = errorCall
214+
expect(eventName).toBe('error')
215+
expect(variable).toEqual(b)
216+
expect(errorMessage).toBe('oopsy whoopsies')
207217
})
208218

209219
test('works with map template variables', async () => {
@@ -356,4 +366,33 @@ describe('hydrate vars', () => {
356366
}).promise
357367
expect(actual.length).toEqual(2)
358368
})
369+
370+
it('fires an error event when an error is caught', async () => {
371+
const error = 'four_oh_four not found'
372+
const fetcher = new FakeFetcher()
373+
const queryVariable = createVariable(
374+
'errorVariable',
375+
'f(x: v.four_oh_four)',
376+
null,
377+
RemoteDataState.Error
378+
)
379+
380+
fetcher.setResponse(queryVariable, Promise.reject(error))
381+
382+
const actual = hydrateVars([queryVariable], [queryVariable], {
383+
url: '',
384+
orgID: '',
385+
selections: {},
386+
fetcher,
387+
})
388+
const mockFire = jest.spyOn(actual.on, 'fire')
389+
390+
await actual.promise
391+
392+
const [_, errorCall] = mockFire.mock.calls
393+
const [eventName, variable, errorMessage] = errorCall
394+
expect(eventName).toBe('error')
395+
expect(variable).toEqual(queryVariable)
396+
expect(errorMessage).toBe(error)
397+
})
359398
})

src/variables/utils/hydrateVars.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,11 +338,13 @@ const invalidateCycles = (graph: VariableNode[]): void => {
338338
/*
339339
Given a node, mark all ancestors of that node as `Error`.
340340
*/
341-
const invalidateAncestors = (node: VariableNode): void => {
341+
const invalidateAncestors = (node: VariableNode, e: Error, on: any): void => {
342342
const ancestors = collectAncestors(node)
343343

344344
for (const ancestor of ancestors) {
345345
ancestor.status = RemoteDataState.Error
346+
on.fire('error', ancestor.variable, e)
347+
346348
if (ancestor.variable.arguments.type === 'query') {
347349
ancestor.variable.arguments.values.results = []
348350
}
@@ -465,7 +467,8 @@ export const hydrateVars = (
465467
node.status = RemoteDataState.Error
466468
node.variable.arguments.values.results = []
467469

468-
invalidateAncestors(node)
470+
on.fire('error', node.variable, e)
471+
invalidateAncestors(node, e, on)
469472
}
470473
}
471474

0 commit comments

Comments
 (0)