From 73c9f3996ae628cbb6c88f0ef6baad6f7a3f3541 Mon Sep 17 00:00:00 2001 From: Maciej Bodek Date: Fri, 21 Jun 2024 12:41:30 +0200 Subject: [PATCH 1/8] QueryBuilder: sort tables alphabetically --- src/components/queryBuilder/TableSelect.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/queryBuilder/TableSelect.tsx b/src/components/queryBuilder/TableSelect.tsx index 75bc566..54c255f 100644 --- a/src/components/queryBuilder/TableSelect.tsx +++ b/src/components/queryBuilder/TableSelect.tsx @@ -21,7 +21,7 @@ export const TableSelect = (props: Props) => { useEffect(() => { async function fetchTables() { - const tables = await datasource.fetchTables(); + const tables = (await datasource.fetchTables()).sort((a, b) => a.tableName.localeCompare(b.tableName)); const values = tables.map((t) => ({ label: t.tableName, value: t.tableName })); // Add selected value to the list if it does not exist. if (table && !tables.find((x) => x.tableName === table) && props.mode !== BuilderMode.Trend) { From 6085c64c0798dda9eda116ed6887c739c95668f8 Mon Sep 17 00:00:00 2001 From: Maciej Bodek Date: Fri, 21 Jun 2024 12:47:58 +0200 Subject: [PATCH 2/8] QueryBuilder: Remove the default row limit --- src/components/queryBuilder/Limit.tsx | 2 +- src/components/queryBuilder/QueryBuilder.tsx | 2 +- src/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/queryBuilder/Limit.tsx b/src/components/queryBuilder/Limit.tsx index 85fa412..7659a85 100644 --- a/src/components/queryBuilder/Limit.tsx +++ b/src/components/queryBuilder/Limit.tsx @@ -8,7 +8,7 @@ interface LimitEditorProps { onLimitChange: (limit: string) => void; } export const LimitEditor = (props: LimitEditorProps) => { - const [limit, setLimit] = useState(props.limit || '100'); + const [limit, setLimit] = useState(props.limit); const { label, tooltip } = selectors.components.QueryEditor.QueryBuilder.LIMIT; return ( diff --git a/src/components/queryBuilder/QueryBuilder.tsx b/src/components/queryBuilder/QueryBuilder.tsx index 588a6aa..75c294f 100644 --- a/src/components/queryBuilder/QueryBuilder.tsx +++ b/src/components/queryBuilder/QueryBuilder.tsx @@ -295,7 +295,7 @@ export const QueryBuilder = (props: QueryBuilderProps) => { fieldsList={getOrderByFields(builder, fieldsList)} /> - + ) : null; diff --git a/src/types.ts b/src/types.ts index 7cf38f0..c6c253c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -313,7 +313,7 @@ export const defaultBuilderQuery: Omit = { builderOptions: { mode: BuilderMode.List, fields: [], - limit: '100', + limit: '', timeField: '', }, format: Format.TABLE, From 822a3c47861cba6cf8a811e7d6c1dcf3b32c0176 Mon Sep 17 00:00:00 2001 From: Maciej Bodek Date: Fri, 21 Jun 2024 13:26:11 +0200 Subject: [PATCH 3/8] QueryBuilder: Escape fields with number at the start --- src/components/queryBuilder/utils.ts | 253 +++++++++++++++------------ 1 file changed, 139 insertions(+), 114 deletions(-) diff --git a/src/components/queryBuilder/utils.ts b/src/components/queryBuilder/utils.ts index 24f6daa..1d05587 100644 --- a/src/components/queryBuilder/utils.ts +++ b/src/components/queryBuilder/utils.ts @@ -1,14 +1,18 @@ import { astVisitor, Expr, - ExprBinary, ExprBool, - ExprCall, ExprCast, + ExprBinary, + ExprBool, + ExprCall, + ExprCast, ExprInteger, - ExprList, ExprNumeric, + ExprList, + ExprNumeric, ExprRef, ExprString, ExprUnary, - FromTable, IAstVisitor, + FromTable, + IAstVisitor, SelectedColumn, } from '@questdb/sql-ast-parser'; import { @@ -26,19 +30,20 @@ import { OrderBy, SampleByAlignToMode, SqlBuilderOptions, - SqlBuilderOptionsAggregate, SqlBuilderOptionsList, + SqlBuilderOptionsAggregate, + SqlBuilderOptionsList, SqlBuilderOptionsTrend, } from 'types'; -import {sqlToStatement} from 'data/ast'; -import {Datasource} from "../../data/QuestDbDatasource"; -import {isString} from "lodash"; +import { sqlToStatement } from 'data/ast'; +import { Datasource } from '../../data/QuestDbDatasource'; +import { isString } from 'lodash'; export const isBooleanType = (type: string): boolean => { return ['boolean'].includes(type?.toLowerCase()); }; export const isGeoHashType = (type: string): boolean => { - return type?.toLowerCase().startsWith("geohash") + return type?.toLowerCase().startsWith('geohash'); }; export const isNumberType = (type: string): boolean => { @@ -84,7 +89,7 @@ export const isDateFilter = (filter: Filter): filter is DateFilter => { }; export const isMultiFilter = (filter: Filter): filter is MultiFilter => { - return FilterOperator.In === filter.operator || FilterOperator.NotIn === filter.operator; + return FilterOperator.In === filter.operator || FilterOperator.NotIn === filter.operator; }; export const isSetFilter = (filter: Filter): filter is MultiFilter => { @@ -96,13 +101,13 @@ const getListQuery = (table = '', fields: string[] = []): string => { return `SELECT ${fields.join(', ')} FROM ${escaped(table)}`; }; -const getLatestOn = (timeField = '', partitionBy: string[] = []): string => { - if ( timeField.length === 0 || partitionBy.length === 0 ){ +const getLatestOn = (timeField = '', partitionBy: string[] = []): string => { + if (timeField.length === 0 || partitionBy.length === 0) { return ''; } return ` LATEST ON ${timeField} PARTITION BY ${partitionBy.join(', ')}`; -} +}; const getAggregationQuery = ( table = '', @@ -154,7 +159,7 @@ const getSampleByQuery = ( return `SELECT ${metricsQuery} FROM ${escaped(table)}`; }; -const getFilters = (filters: Filter[]): {filters: string; hasTimeFilter: boolean} => { +const getFilters = (filters: Filter[]): { filters: string; hasTimeFilter: boolean } => { let hasTsFilter = false; let combinedFilters = filters.reduce((previousValue, currentFilter, currentIndex) => { @@ -170,26 +175,26 @@ const getFilters = (filters: Filter[]): {filters: string; hasTimeFilter: boolean } else if (currentFilter.operator === FilterOperator.OutsideGrafanaTimeRange) { operator = ''; notOperator = true; - field = ` \$__timeFilter(${currentFilter.key})` + field = ` \$__timeFilter(${currentFilter.key})`; hasTsFilter = true; } else if (FilterOperator.WithInGrafanaTimeRange === currentFilter.operator) { operator = ''; - field = ` \$__timeFilter(${currentFilter.key})` + field = ` \$__timeFilter(${currentFilter.key})`; hasTsFilter = true; } else { operator = currentFilter.operator; } - if ( operator.length > 0 ){ + if (operator.length > 0) { filter = ` ${field} ${operator}`; } else { - filter = ` ${field}` + filter = ` ${field}`; } if (isNullFilter(currentFilter)) { // don't add anything } else if (isMultiFilter(currentFilter)) { let values = currentFilter.value; - if (isNumberType(currentFilter.type)){ + if (isNumberType(currentFilter.type)) { filter += ` (${values?.map((v) => v.trim()).join(', ')} )`; } else { filter += ` (${values?.map((v) => formatStringValue(v).trim()).join(', ')} )`; @@ -202,56 +207,58 @@ const getFilters = (filters: Filter[]): {filters: string; hasTimeFilter: boolean if (!isDateFilterWithOutValue(currentFilter)) { switch (currentFilter.value) { case 'GRAFANA_START_TIME': - filter += ` \$__fromTime`; + filter += ` \$__fromTime`; break; case 'GRAFANA_END_TIME': - filter += ` \$__toTime`; + filter += ` \$__toTime`; break; default: filter += ` ${currentFilter.value || 'TODAY'}`; } } } else { - filter += formatStringValue(currentFilter.value || ''); + filter += formatStringValue(currentFilter.value || ''); } if (notOperator) { filter = ` NOT (${filter} )`; } - if ( !filter ){ + if (!filter) { return previousValue; } - if ( previousValue.length > 0 ){ - return `${previousValue} ${prefixCondition}${filter}` + if (previousValue.length > 0) { + return `${previousValue} ${prefixCondition}${filter}`; } else { return filter; } }, ''); - return { filters: combinedFilters, hasTimeFilter: hasTsFilter } + return { filters: combinedFilters, hasTimeFilter: hasTsFilter }; }; const getSampleBy = (sampleByMode: SampleByAlignToMode, sampleByValue?: string, sampleByFill?: string[]): string => { - let fills = ''; - if (sampleByFill !== undefined && sampleByFill.length > 0){ + if (sampleByFill !== undefined && sampleByFill.length > 0) { // remove suffixes - fills = ` FILL ( ${sampleByFill.map((s)=>s.replace(/_[0-9]+$/, '')).join(', ')} )`; + fills = ` FILL ( ${sampleByFill.map((s) => s.replace(/_[0-9]+$/, '')).join(', ')} )`; } let mode = ''; - if (sampleByMode !== undefined ){ + if (sampleByMode !== undefined) { mode = ` ALIGN TO ${sampleByMode}`; } let offsetOrTz = ''; - if ( (sampleByMode === SampleByAlignToMode.CalendarOffset || sampleByMode === SampleByAlignToMode.CalendarTimeZone) && - sampleByValue !== undefined && sampleByValue.length > 0 ){ + if ( + (sampleByMode === SampleByAlignToMode.CalendarOffset || sampleByMode === SampleByAlignToMode.CalendarTimeZone) && + sampleByValue !== undefined && + sampleByValue.length > 0 + ) { offsetOrTz = ` '${sampleByValue}'`; } return ` SAMPLE BY \$__sampleByInterval${fills}${mode}${offsetOrTz}`; -} +}; const getGroupBy = (groupBy: string[] = [], timeField?: string): string => { const clause = groupBy.length > 0 ? ` GROUP BY ${groupBy.join(', ')}` : ''; @@ -281,12 +288,19 @@ const getLimit = (limit?: string): string => { return ` LIMIT ` + (limit || '100'); }; +const escapeFields = (fields: string[]): string[] => { + return fields.map((f) => { + return f.match(/^\d/) ? `"${f}"` : f; + }); +}; + export const getSQLFromQueryOptions = (options: SqlBuilderOptions): string => { const limit = options.limit ? getLimit(options.limit) : ''; + const fields = escapeFields(options.fields || []); let query = ``; switch (options.mode) { case BuilderMode.Aggregate: - query += getAggregationQuery(options.table, options.fields, options.metrics, options.groupBy); + query += getAggregationQuery(options.table, fields, options.metrics, options.groupBy); const aggregateFilters = getFilters(options.filters || []); if (aggregateFilters.filters) { query += ` WHERE${aggregateFilters.filters}`; @@ -294,20 +308,14 @@ export const getSQLFromQueryOptions = (options: SqlBuilderOptions): string => { query += getGroupBy(options.groupBy); break; case BuilderMode.Trend: - query += getSampleByQuery( - options.table, - options.fields, - options.metrics, - options.groupBy, - options.timeField - ); + query += getSampleByQuery(options.table, fields, options.metrics, options.groupBy, options.timeField); const sampleByFilters = getFilters(options.filters || []); - if ( options.timeField || sampleByFilters.filters.length > 0 ){ + if (options.timeField || sampleByFilters.filters.length > 0) { query += ' WHERE'; - if ( options.timeField && !sampleByFilters.hasTimeFilter ){ + if (options.timeField && !sampleByFilters.hasTimeFilter) { query += ` $__timeFilter(${options.timeField})`; - if ( sampleByFilters.filters.length > 0 ){ + if (sampleByFilters.filters.length > 0) { query += ' AND'; } } @@ -319,7 +327,7 @@ export const getSQLFromQueryOptions = (options: SqlBuilderOptions): string => { break; case BuilderMode.List: default: - query += getListQuery(options.table, options.fields); + query += getListQuery(options.table, fields); const filters = getFilters(options.filters || []); if (filters.filters) { query += ` WHERE${filters.filters}`; @@ -333,10 +341,13 @@ export const getSQLFromQueryOptions = (options: SqlBuilderOptions): string => { return query; }; -export async function getQueryOptionsFromSql(sql: string, datasource?: Datasource): Promise { +export async function getQueryOptionsFromSql( + sql: string, + datasource?: Datasource +): Promise { const ast = sqlToStatement(sql); if (!ast || ast.type !== 'select') { - return 'The query can\'t be parsed.'; + return "The query can't be parsed."; } if (!ast.from || ast.from.length !== 1) { return `The query has too many 'FROM' clauses.`; @@ -349,14 +360,16 @@ export async function getQueryOptionsFromSql(sql: string, datasource?: Datasourc let timeField; let fieldsToTypes = new Map(); - if ( fromTable?.name?.name.length > 0 && datasource ){ - const dbFields = await datasource.fetchFields(fromTable?.name?.name); - dbFields.forEach((f)=>{ fieldsToTypes.set(f.name, f.type) }); - timeField = dbFields.find( (f) => f.designated)?.name; + if (fromTable?.name?.name.length > 0 && datasource) { + const dbFields = await datasource.fetchFields(fromTable?.name?.name); + dbFields.forEach((f) => { + fieldsToTypes.set(f.name, f.type); + }); + timeField = dbFields.find((f) => f.designated)?.name; } - if ( timeField === undefined ){ - timeField = ""; + if (timeField === undefined) { + timeField = ''; } const fieldsAndMetrics = getMetricsFromAst(ast.columns ? ast.columns : null); @@ -364,7 +377,7 @@ export async function getQueryOptionsFromSql(sql: string, datasource?: Datasourc let builder = { mode: BuilderMode.List, table: fromTable.name.name, - timeField: timeField + timeField: timeField, } as SqlBuilderOptions; if (fieldsAndMetrics.fields) { @@ -394,9 +407,9 @@ export async function getQueryOptionsFromSql(sql: string, datasource?: Datasourc } builder.limit = undefined; - if (ast.limit){ - if (ast.limit.upperBound && ast.limit.upperBound.type === 'integer'){ - if (ast.limit.lowerBound && ast.limit.lowerBound.type === 'integer'){ + if (ast.limit) { + if (ast.limit.upperBound && ast.limit.upperBound.type === 'integer') { + if (ast.limit.lowerBound && ast.limit.lowerBound.type === 'integer') { builder.limit = `${ast.limit.lowerBound.value}, ${ast.limit.upperBound.value}`; } else { builder.limit = `${ast.limit.upperBound.value}`; @@ -404,32 +417,32 @@ export async function getQueryOptionsFromSql(sql: string, datasource?: Datasourc } } - if (ast.sampleBy){ + if (ast.sampleBy) { builder.mode = BuilderMode.Trend; - if (ast.sampleByAlignTo){ + if (ast.sampleByAlignTo) { (builder as SqlBuilderOptionsTrend).sampleByAlignTo = ast.sampleByAlignTo.alignTo as SampleByAlignToMode; } - if (ast.sampleByFill){ - (builder as SqlBuilderOptionsTrend).sampleByFill = ast.sampleByFill.map( f => { - if ( f.type === 'sampleByKeyword' ){ + if (ast.sampleByFill) { + (builder as SqlBuilderOptionsTrend).sampleByFill = ast.sampleByFill.map((f) => { + if (f.type === 'sampleByKeyword') { return f.keyword; - } else if ( f.type === 'null'){ + } else if (f.type === 'null') { return 'null'; } else { return f.value.toString(); } }); } - if (ast.sampleByAlignTo?.alignValue){ + if (ast.sampleByAlignTo?.alignValue) { (builder as SqlBuilderOptionsTrend).sampleByAlignToValue = ast.sampleByAlignTo?.alignValue; } } - if (ast.latestOn){ + if (ast.latestOn) { builder.mode = BuilderMode.List; - if (ast.partitionBy){ + if (ast.partitionBy) { (builder as SqlBuilderOptionsList).partitionBy = ast.partitionBy.map((p) => { - if (p.table){ + if (p.table) { return p.table.name + '.' + p.name; } else { return p.name; @@ -458,7 +471,7 @@ type MapperState = { filters: Filter[]; notFlag: boolean; condition: 'AND' | 'OR' | null; -} +}; function getFiltersFromAst(expr: Expr, fieldsToTypes: Map): Filter[] { let state: MapperState = { currentFilter: null, filters: [], notFlag: false, condition: null } as MapperState; @@ -503,9 +516,10 @@ function getFiltersFromAst(expr: Expr, fieldsToTypes: Map): Filt }, })); - try {// don't break conversion + try { + // don't break conversion visitor.expr(expr); - } catch ( error ){ + } catch (error) { console.error(error); } @@ -514,32 +528,32 @@ function getFiltersFromAst(expr: Expr, fieldsToTypes: Map): Filt function getRefFilter(e: ExprRef, state: MapperState, fieldsToTypes: Map) { let doAdd = false; - if ( state.currentFilter === null){ + if (state.currentFilter === null) { state.currentFilter = {} as Filter; doAdd = true; } - if ( e.name?.toLowerCase() === '$__fromtime'){ - state.currentFilter = { ...state.currentFilter, value: 'GRAFANA_START_TIME', type: 'timestamp' } as Filter; + if (e.name?.toLowerCase() === '$__fromtime') { + state.currentFilter = { ...state.currentFilter, value: 'GRAFANA_START_TIME', type: 'timestamp' } as Filter; return; } - if ( e.name?.toLowerCase() === '$__totime'){ + if (e.name?.toLowerCase() === '$__totime') { state.currentFilter = { ...state.currentFilter, value: 'GRAFANA_END_TIME', type: 'timestamp' } as Filter; return; } let type = fieldsToTypes.get(e.name); - if ( !state.currentFilter.key ) { - state.currentFilter = { ...state.currentFilter, key: e.name} ; - if (type){ + if (!state.currentFilter.key) { + state.currentFilter = { ...state.currentFilter, key: e.name }; + if (type) { state.currentFilter.type = type; } } else { state.currentFilter = { ...state.currentFilter, value: [e.name], type: type || 'string' } as Filter; } - if ( doAdd ){ + if (doAdd) { state.filters.push(state.currentFilter); state.currentFilter = null; } @@ -556,25 +570,27 @@ function getListFilter(e: ExprList, state: MapperState) { } as Filter; } -function getCallString(e: ExprCall){ - let args: string = e.args.map((x) =>{ - switch (x.type){ - case 'string': - return `'${x.value}'`; - case 'boolean': - case 'numeric': - case 'integer': - return x.value; - case 'ref': - return x.name; - case 'null': - return 'null'; - case 'call': - return getCallString(x); - default: - return '' - } - }).join(', '); +function getCallString(e: ExprCall) { + let args: string = e.args + .map((x) => { + switch (x.type) { + case 'string': + return `'${x.value}'`; + case 'boolean': + case 'numeric': + case 'integer': + return x.value; + case 'ref': + return x.name; + case 'null': + return 'null'; + case 'call': + return getCallString(x); + default: + return ''; + } + }) + .join(', '); return `${e.function.name}(${args})`; } @@ -594,21 +610,23 @@ function toString(x: Expr) { case 'call': return getCallString(x); default: - return '' + return ''; } } function getCallFilter(e: ExprCall, state: MapperState) { let doAdd = false; - if ( !state.currentFilter ){ + if (!state.currentFilter) { // map f(x) to true = f(x) so it can be displayed in builder - state.currentFilter = {key: 'true', type: 'boolean'} as Filter; + state.currentFilter = { key: 'true', type: 'boolean' } as Filter; doAdd = true; } - let args = e.args.map((x) =>{ - return toString(x); - }).join(', '); + let args = e.args + .map((x) => { + return toString(x); + }) + .join(', '); const val = `${e.function.name}(${args})`; if (val.startsWith('$__timefilter(')) { @@ -622,8 +640,8 @@ function getCallFilter(e: ExprCall, state: MapperState) { state.currentFilter = { ...state.currentFilter, value: val } as Filter; } - if ( doAdd ){ - if (state.condition){ + if (doAdd) { + if (state.condition) { state.currentFilter.condition = state.condition; state.condition = null; } @@ -642,7 +660,7 @@ function getUnaryFilter(mapper: IAstVisitor, e: ExprUnary, state: MapperState) { } state.currentFilter = { operator: e.op as FilterOperator } as Filter; - if ( state.condition ){ + if (state.condition) { state.currentFilter.condition = state.condition; state.condition = null; } @@ -656,7 +674,11 @@ function getStringFilter(e: ExprString, state: MapperState) { state.currentFilter = { ...state.currentFilter, key: e.value } as Filter; return; } - state.currentFilter = { ...state.currentFilter, value: e.value, type: state.currentFilter?.type || 'string' } as Filter; + state.currentFilter = { + ...state.currentFilter, + value: e.value, + type: state.currentFilter?.type || 'string', + } as Filter; } function getNumericFilter(e: ExprNumeric, state: MapperState) { @@ -679,14 +701,13 @@ function getCastFilter(e: ExprCast, state: MapperState) { let val = `cast( ${toString(e.operand)} as ${e.to.kind === undefined ? e.to.name : ''} )`; if (state.currentFilter != null && !state.currentFilter.key) { - state.currentFilter = {...state.currentFilter, key: val} as Filter; + state.currentFilter = { ...state.currentFilter, key: val } as Filter; return; } else { - state.currentFilter = {...state.currentFilter, value: val, type: state.currentFilter?.type || 'int'} as Filter; + state.currentFilter = { ...state.currentFilter, value: val, type: state.currentFilter?.type || 'int' } as Filter; } } - function getBooleanFilter(e: ExprBool, state: MapperState) { state.currentFilter = { ...state.currentFilter, value: e.value, type: 'boolean' } as Filter; } @@ -700,7 +721,7 @@ function getBinaryFilter(mapper: IAstVisitor, e: ExprBinary, state: MapperState) } else if (Object.values(FilterOperator).find((x) => e.op === x)) { state.currentFilter = {} as Filter; state.currentFilter.operator = e.op as FilterOperator; - if ( state.condition ){ + if (state.condition) { state.currentFilter.condition = state.condition; state.condition = null; } @@ -725,7 +746,11 @@ function selectCallFunc(s: SelectedColumn): BuilderMetricField | string { } return x.name; }); - if ( Object.values(BuilderMetricFieldAggregation).includes( s.expr.function.name.toLowerCase() as BuilderMetricFieldAggregation ) ) { + if ( + Object.values(BuilderMetricFieldAggregation).includes( + s.expr.function.name.toLowerCase() as BuilderMetricFieldAggregation + ) + ) { return { aggregation: s.expr.function.name as BuilderMetricFieldAggregation, field: fields[0], @@ -770,7 +795,7 @@ function getMetricsFromAst(selectClauses: SelectedColumn[] | null): { fields.push(`${s.expr.value}`); break; case 'cast': - fields.push(`cast(${toString(s.expr.operand)} as ${s.expr.to.kind === undefined ? s.expr.to?.name : '' })`) + fields.push(`cast(${toString(s.expr.operand)} as ${s.expr.to.kind === undefined ? s.expr.to?.name : ''})`); break; default: break; @@ -780,7 +805,7 @@ function getMetricsFromAst(selectClauses: SelectedColumn[] | null): { } function formatStringValue(currentFilter: string): string { - if ( Array.isArray(currentFilter) ){ + if (Array.isArray(currentFilter)) { currentFilter = currentFilter[0]; } if (currentFilter.startsWith('$')) { From d171bbac5b5301b265b6205f9b89968862b2cd88 Mon Sep 17 00:00:00 2001 From: Maciej Bodek Date: Fri, 21 Jun 2024 13:41:28 +0200 Subject: [PATCH 4/8] Escape all operators in fields --- src/components/queryBuilder/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/queryBuilder/utils.ts b/src/components/queryBuilder/utils.ts index 1d05587..74cffbc 100644 --- a/src/components/queryBuilder/utils.ts +++ b/src/components/queryBuilder/utils.ts @@ -290,7 +290,7 @@ const getLimit = (limit?: string): string => { const escapeFields = (fields: string[]): string[] => { return fields.map((f) => { - return f.match(/^\d/) ? `"${f}"` : f; + return f.match(/(^\d|\s|\$|\&|\|)/im) ? `"${f}"` : f; }); }; From b106e498e20690f6e426ccf796cc55a8b12fa208 Mon Sep 17 00:00:00 2001 From: Maciej Bodek Date: Fri, 21 Jun 2024 13:47:41 +0200 Subject: [PATCH 5/8] Escape if all non-letter characters are present --- src/components/queryBuilder/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/queryBuilder/utils.ts b/src/components/queryBuilder/utils.ts index 74cffbc..32e7e96 100644 --- a/src/components/queryBuilder/utils.ts +++ b/src/components/queryBuilder/utils.ts @@ -290,7 +290,7 @@ const getLimit = (limit?: string): string => { const escapeFields = (fields: string[]): string[] => { return fields.map((f) => { - return f.match(/(^\d|\s|\$|\&|\|)/im) ? `"${f}"` : f; + return f.match(/(^\d|[^a-zA-Z_])/im) ? `"${f}"` : f; }); }; From 33137027f14622ae715a70bd45a367e0686edf8a Mon Sep 17 00:00:00 2001 From: Maciej Bodek Date: Fri, 21 Jun 2024 14:59:01 +0200 Subject: [PATCH 6/8] Add docs link in Readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index a53400a..b4a5568 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ data from within Grafana. For detailed instructions on how to install the plugin on Grafana Cloud or locally, please check out the [Plugin installation docs](https://grafana.com/docs/grafana/latest/plugins/installation/). +Read the guide on QuestDB website: [Third-party Tools - Grafana](https://questdb.io/docs/third-party-tools/grafana/). + ## Configuration ### QuestDB user for the data source @@ -179,3 +181,4 @@ You may choose to hide this variable from view as it serves no further purpose. - Configure and use [Templates and variables](https://grafana.com/docs/grafana/latest/variables/). - Add [Transformations](https://grafana.com/docs/grafana/latest/panels/transformations/). - Set up alerting; refer to [Alerts overview](https://grafana.com/docs/grafana/latest/alerting/). +- Read the [Plugin guide](https://questdb.io/docs/third-party-tools/grafana/) on QuestDB website From 52c583c93e6321048ef00e3102ac8de469946e10 Mon Sep 17 00:00:00 2001 From: Maciej Bodek Date: Mon, 24 Jun 2024 11:52:44 +0200 Subject: [PATCH 7/8] Disable sample by and latest on with no ts --- src/components/queryBuilder/GroupBy.tsx | 4 +++- src/components/queryBuilder/QueryBuilder.tsx | 8 +++++++- src/components/queryBuilder/SampleByFillEditor.tsx | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/queryBuilder/GroupBy.tsx b/src/components/queryBuilder/GroupBy.tsx index ddec532..71d3db3 100644 --- a/src/components/queryBuilder/GroupBy.tsx +++ b/src/components/queryBuilder/GroupBy.tsx @@ -10,6 +10,7 @@ interface GroupByEditorProps { groupBy: string[]; onGroupByChange: (groupBy: string[]) => void; labelAndTooltip: typeof selectors.components.QueryEditor.QueryBuilder.GROUP_BY; + isDisabled: boolean; } export const GroupByEditor = (props: GroupByEditorProps) => { const columns: SelectableValue[] = (props.fieldsList || []).map((f) => ({ label: f.label, value: f.name })); @@ -34,7 +35,7 @@ export const GroupByEditor = (props: GroupByEditorProps) => { setIsOpen(true)} onCloseMenu={() => setIsOpen(false)} @@ -43,6 +44,7 @@ export const GroupByEditor = (props: GroupByEditorProps) => { value={groupBy} allowCustomValue={true} width={50} + disabled={props.isDisabled} /> ); diff --git a/src/components/queryBuilder/QueryBuilder.tsx b/src/components/queryBuilder/QueryBuilder.tsx index 75c294f..a480919 100644 --- a/src/components/queryBuilder/QueryBuilder.tsx +++ b/src/components/queryBuilder/QueryBuilder.tsx @@ -230,6 +230,7 @@ export const QueryBuilder = (props: QueryBuilderProps) => { onGroupByChange={onGroupByChange} fieldsList={fieldsList} labelAndTooltip={selectors.components.QueryEditor.QueryBuilder.SAMPLE_BY} + isDisabled={builder.timeField.length === 0} /> )} @@ -264,7 +265,11 @@ export const QueryBuilder = (props: QueryBuilderProps) => { {builder.mode === BuilderMode.Trend && ( - + )} @@ -275,6 +280,7 @@ export const QueryBuilder = (props: QueryBuilderProps) => { onGroupByChange={onGroupByChange} fieldsList={fieldsList} labelAndTooltip={selectors.components.QueryEditor.QueryBuilder.GROUP_BY} + isDisabled={builder.timeField.length === 0} /> )} diff --git a/src/components/queryBuilder/SampleByFillEditor.tsx b/src/components/queryBuilder/SampleByFillEditor.tsx index 45b4d21..6fa02c9 100644 --- a/src/components/queryBuilder/SampleByFillEditor.tsx +++ b/src/components/queryBuilder/SampleByFillEditor.tsx @@ -9,6 +9,7 @@ import { GroupBase, OptionsOrGroups } from 'react-select'; interface FillEditorProps { fills: string[]; onFillsChange: (fills: string[]) => void; + isDisabled: boolean; } const fillModes: SelectableValue[] = []; @@ -85,6 +86,8 @@ export const SampleByFillEditor = (props: FillEditorProps) => { width={50} isClearable={true} hideSelectedOptions={true} + placeholder={props.isDisabled ? 'Table is missing designated timestamp' : 'Choose'} + disabled={props.isDisabled} /> ); From 74aff812c030f12de7c1fcae084f931e9a8d7131 Mon Sep 17 00:00:00 2001 From: Maciej Bodek Date: Mon, 24 Jun 2024 12:08:11 +0200 Subject: [PATCH 8/8] Update tests --- src/components/queryBuilder/GroupBy.test.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/queryBuilder/GroupBy.test.tsx b/src/components/queryBuilder/GroupBy.test.tsx index d040215..bb0363a 100644 --- a/src/components/queryBuilder/GroupBy.test.tsx +++ b/src/components/queryBuilder/GroupBy.test.tsx @@ -1,11 +1,19 @@ import React from 'react'; import { render } from '@testing-library/react'; import { GroupByEditor } from './GroupBy'; -import {selectors} from "../../selectors"; +import { selectors } from '../../selectors'; describe('GroupByEditor', () => { it('renders correctly', () => { - const result = render( {}} labelAndTooltip={selectors.components.QueryEditor.QueryBuilder.SAMPLE_BY} />); + const result = render( + {}} + isDisabled={false} + labelAndTooltip={selectors.components.QueryEditor.QueryBuilder.SAMPLE_BY} + /> + ); expect(result.container.firstChild).not.toBeNull(); }); });