diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8e0139f64..559725aa7e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ 1. [12791](https://github.com/influxdata/influxdb/pull/12791): Use time range for metaqueries in Data Explorer and Cell Editor Overlay 1. [12827](https://github.com/influxdata/influxdb/pull/12827): Fix screen tearing bug in Raw Data View 1. [12843](https://github.com/influxdata/influxdb/pull/12843): Add copy to clipboard button to export overlays +1. [12826](https://github.com/influxdata/influxdb/pull/12826): Enable copying error messages to the clipboard from dashboard cells ### Bug Fixes diff --git a/ui/src/shared/components/EmptyGraphError.scss b/ui/src/shared/components/EmptyGraphError.scss index 55f90280ba4..0c9ab94d57c 100644 --- a/ui/src/shared/components/EmptyGraphError.scss +++ b/ui/src/shared/components/EmptyGraphError.scss @@ -23,4 +23,9 @@ top: $ix-marg-b; right: $ix-marg-b; opacity: 0.9; -} \ No newline at end of file + display: none; + + .empty-graph-error:hover & { + display: inherit; + } +} diff --git a/ui/src/shared/components/TimeSeries.tsx b/ui/src/shared/components/TimeSeries.tsx index f7774a45485..b444d65fb14 100644 --- a/ui/src/shared/components/TimeSeries.tsx +++ b/ui/src/shared/components/TimeSeries.tsx @@ -12,6 +12,7 @@ import { // Utils import {parseResponse} from 'src/shared/parsing/flux/response' import {getActiveOrg} from 'src/organizations/selectors' +import {checkQueryResult} from 'src/shared/utils/checkQueryResult' // Types import {RemoteDataState, FluxTable} from 'src/types' @@ -137,6 +138,8 @@ class TimeSeries extends Component { const tables = flatten(results.map(r => parseResponse(r.csv))) const files = results.map(r => r.csv) + files.forEach(checkQueryResult) + this.setState({ tables, files, diff --git a/ui/src/shared/utils/checkQueryResult.test.ts b/ui/src/shared/utils/checkQueryResult.test.ts new file mode 100644 index 00000000000..445369f489e --- /dev/null +++ b/ui/src/shared/utils/checkQueryResult.test.ts @@ -0,0 +1,28 @@ +import {checkQueryResult} from 'src/shared/utils/checkQueryResult' + +describe('checkQueryResult', () => { + test('throws an error when the response has an error table', () => { + const RESPONSE = `#group,true,true +#datatype,string,string +#default,, +,error,reference +,"function references unknown column ""_value""",` + + expect(() => { + checkQueryResult(RESPONSE) + }).toThrow('function references unknown column') + }) + + test('does not throw an error when the response is valid', () => { + const RESPONSE = `#group,false,false,true,true,false,false,true,true,true +#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string +#default,_result,,,,,,,, +,result,table,_start,_stop,_time,_value,_measurement,host,_field +,,0,2019-03-21T18:54:14.113478Z,2019-03-21T19:54:14.113478Z,2019-03-21T18:54:21Z,4780101632,mem,oox4k.local,active +,,0,2019-03-21T18:54:14.113478Z,2019-03-21T19:54:14.113478Z,2019-03-21T18:54:31Z,5095436288,mem,oox4k.local,active` + + expect(() => { + checkQueryResult(RESPONSE) + }).not.toThrow() + }) +}) diff --git a/ui/src/shared/utils/checkQueryResult.ts b/ui/src/shared/utils/checkQueryResult.ts new file mode 100644 index 00000000000..d3b5a3152df --- /dev/null +++ b/ui/src/shared/utils/checkQueryResult.ts @@ -0,0 +1,56 @@ +/* + Given Flux query response as a CSV, check if the CSV contains an error table + as the first result. If it does, throw the error message contained within + that table. + + For example, given the following response: + + #datatype,string,long + ,error,reference + ,Failed to parse query,897 + + we want to throw an error with the message "Failed to parse query". + + See https://github.com/influxdata/flux/blob/master/docs/SPEC.md#errors. +*/ +export const checkQueryResult = (file: string): void => { + // Don't check the whole file, since it could be huge and the error table + // will be within the first few lines (if it exists) + const fileHead = file.slice(0, findNthIndex(file, '\n', 6)) + + const lines = fileHead.split('\n').filter(line => !line.startsWith('#')) + + if (!lines.length || !lines[0].includes('error') || !lines[1]) { + return + } + + const header = lines[0].split(',').map(s => s.trim()) + const row = lines[1].split(',').map(s => s.trim()) + const index = header.indexOf('error') + + if (index === -1 || !row[index]) { + return + } + + // Trim off extra quotes at start and end of message + const errorMessage = row[index].replace(/^"/, '').replace(/"$/, '') + + throw new Error(errorMessage) +} + +const findNthIndex = (s: string, c: string, n: number) => { + let count = 0 + let i = 0 + + while (i < s.length) { + if (s[i] == c) { + count += 1 + } + + if (count === n) { + return i + } + + i += 1 + } +} diff --git a/ui/src/timeMachine/actions/queries.ts b/ui/src/timeMachine/actions/queries.ts index 557a74cecd6..cd78b4e7a1c 100644 --- a/ui/src/timeMachine/actions/queries.ts +++ b/ui/src/timeMachine/actions/queries.ts @@ -13,6 +13,7 @@ import {getActiveOrg} from 'src/organizations/selectors' import {getVariableAssignments} from 'src/variables/selectors' import {getTimeRangeVars} from 'src/variables/utils/getTimeRangeVars' import {filterUnusedVars} from 'src/shared/utils/filterUnusedVars' +import {checkQueryResult} from 'src/shared/utils/checkQueryResult' import { getVariablesForOrg, getVariable, @@ -113,9 +114,10 @@ export const executeQueries = () => async (dispatch, getState: GetState) => { const results = await Promise.all(pendingResults.map(r => r.promise)) const duration = Date.now() - startTime - const files = results.map(r => r.csv) + files.forEach(checkQueryResult) + dispatch(setQueryResults(RemoteDataState.Done, files, duration)) } catch (e) { if (e instanceof CancellationError) {