mirror of https://github.com/grafana/grafana
Azure Monitor: Add logs query builder (#99055)
parent
44ca402116
commit
3b73ebb210
|
@ -0,0 +1,152 @@ |
||||
import React, { useState } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { InputGroup, AccessoryButton } from '@grafana/plugin-ui'; |
||||
import { Select, Label, Input } from '@grafana/ui'; |
||||
|
||||
import { |
||||
BuilderQueryEditorExpressionType, |
||||
BuilderQueryEditorPropertyType, |
||||
BuilderQueryEditorReduceExpression, |
||||
} from '../../dataquery.gen'; |
||||
|
||||
import { aggregateOptions, inputFieldSize } from './utils'; |
||||
|
||||
interface AggregateItemProps { |
||||
aggregate: BuilderQueryEditorReduceExpression; |
||||
columns: Array<SelectableValue<string>>; |
||||
onChange: (item: BuilderQueryEditorReduceExpression) => void; |
||||
onDelete: () => void; |
||||
templateVariableOptions: SelectableValue<string>; |
||||
} |
||||
|
||||
const AggregateItem: React.FC<AggregateItemProps> = ({ |
||||
aggregate, |
||||
onChange, |
||||
onDelete, |
||||
columns, |
||||
templateVariableOptions, |
||||
}) => { |
||||
const isPercentile = aggregate.reduce?.name === 'percentile'; |
||||
const isCountAggregate = aggregate.reduce?.name?.includes('count'); |
||||
|
||||
const [percentileValue, setPercentileValue] = useState(aggregate.parameters?.[0]?.value || ''); |
||||
const [columnValue, setColumnValue] = useState( |
||||
isPercentile ? aggregate.parameters?.[1]?.value || '' : aggregate.property?.name || '' |
||||
); |
||||
|
||||
const safeTemplateVariables = Array.isArray(templateVariableOptions) |
||||
? templateVariableOptions |
||||
: [templateVariableOptions]; |
||||
|
||||
const selectableOptions = columns.concat(safeTemplateVariables); |
||||
|
||||
const buildPercentileParams = (percentile: string, column: string) => [ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Function_parameter, |
||||
fieldType: BuilderQueryEditorPropertyType.Number, |
||||
value: percentile, |
||||
}, |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Function_parameter, |
||||
fieldType: BuilderQueryEditorPropertyType.String, |
||||
value: column, |
||||
}, |
||||
]; |
||||
|
||||
const updateAggregate = (updates: Partial<BuilderQueryEditorReduceExpression>) => { |
||||
const base: BuilderQueryEditorReduceExpression = { |
||||
...aggregate, |
||||
...updates, |
||||
}; |
||||
|
||||
onChange(base); |
||||
}; |
||||
|
||||
const handleAggregateChange = (funcName?: string) => { |
||||
updateAggregate({ |
||||
reduce: { name: funcName || '', type: BuilderQueryEditorPropertyType.Function }, |
||||
}); |
||||
}; |
||||
|
||||
const handlePercentileChange = (value?: string) => { |
||||
const newValue = value || ''; |
||||
setPercentileValue(newValue); |
||||
|
||||
const percentileParams = buildPercentileParams(newValue, columnValue); |
||||
updateAggregate({ parameters: percentileParams }); |
||||
}; |
||||
|
||||
const handleColumnChange = (value?: string) => { |
||||
const newCol = value || ''; |
||||
setColumnValue(newCol); |
||||
|
||||
if (isPercentile) { |
||||
const percentileParams = buildPercentileParams(percentileValue, newCol); |
||||
updateAggregate({ |
||||
parameters: percentileParams, |
||||
property: { |
||||
name: newCol, |
||||
type: BuilderQueryEditorPropertyType.String, |
||||
}, |
||||
}); |
||||
} else { |
||||
updateAggregate({ |
||||
property: { |
||||
name: newCol, |
||||
type: BuilderQueryEditorPropertyType.String, |
||||
}, |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
return ( |
||||
<InputGroup> |
||||
<Select |
||||
aria-label="aggregate function" |
||||
width={inputFieldSize} |
||||
value={aggregate.reduce?.name ? { label: aggregate.reduce.name, value: aggregate.reduce.name } : null} |
||||
options={aggregateOptions} |
||||
onChange={(e) => handleAggregateChange(e.value)} |
||||
/> |
||||
|
||||
{isPercentile ? ( |
||||
<> |
||||
<Input |
||||
type="number" |
||||
min={0} |
||||
max={100} |
||||
step={1} |
||||
value={percentileValue ?? ''} |
||||
width={inputFieldSize} |
||||
onChange={(e) => { |
||||
const val = Number(e.currentTarget.value); |
||||
if (!isNaN(val) && val >= 0 && val <= 100) { |
||||
handlePercentileChange(val.toString()); |
||||
} |
||||
}} |
||||
/> |
||||
<Label style={{ margin: '9px 9px 0 9px' }}>OF</Label> |
||||
</> |
||||
) : ( |
||||
<></> |
||||
)} |
||||
|
||||
{!isCountAggregate ? ( |
||||
<Select |
||||
aria-label="column" |
||||
width={inputFieldSize} |
||||
value={columnValue ? { label: columnValue, value: columnValue } : null} |
||||
options={selectableOptions} |
||||
onChange={(e) => handleColumnChange(e.value)} |
||||
/> |
||||
) : ( |
||||
<></> |
||||
)} |
||||
|
||||
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} /> |
||||
</InputGroup> |
||||
); |
||||
}; |
||||
|
||||
export default AggregateItem; |
@ -0,0 +1,108 @@ |
||||
import React, { useEffect, useRef, useState } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorField, EditorFieldGroup, EditorList, EditorRow } from '@grafana/plugin-ui'; |
||||
|
||||
import { BuilderQueryEditorReduceExpression } from '../../dataquery.gen'; |
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; |
||||
|
||||
import AggregateItem from './AggregateItem'; |
||||
import { BuildAndUpdateOptions } from './utils'; |
||||
|
||||
interface AggregateSectionProps { |
||||
query: AzureMonitorQuery; |
||||
allColumns: AzureLogAnalyticsMetadataColumn[]; |
||||
templateVariableOptions: SelectableValue<string>; |
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void; |
||||
} |
||||
export const AggregateSection: React.FC<AggregateSectionProps> = ({ |
||||
query, |
||||
allColumns, |
||||
buildAndUpdateQuery, |
||||
templateVariableOptions, |
||||
}) => { |
||||
const builderQuery = query.azureLogAnalytics?.builderQuery; |
||||
const [aggregates, setAggregates] = useState<BuilderQueryEditorReduceExpression[]>( |
||||
builderQuery?.reduce?.expressions || [] |
||||
); |
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null); |
||||
const hasLoadedAggregates = useRef(false); |
||||
|
||||
useEffect(() => { |
||||
const currentTable = builderQuery?.from?.property.name || null; |
||||
|
||||
if (prevTable.current !== currentTable || builderQuery?.reduce?.expressions.length === 0) { |
||||
setAggregates([]); |
||||
hasLoadedAggregates.current = false; |
||||
prevTable.current = currentTable; |
||||
} |
||||
}, [builderQuery]); |
||||
|
||||
const availableColumns: Array<SelectableValue<string>> = builderQuery?.columns?.columns?.length |
||||
? builderQuery.columns.columns.map((col) => ({ label: col, value: col })) |
||||
: allColumns.map((col) => ({ label: col.name, value: col.name })); |
||||
|
||||
const onChange = (newItems: Array<Partial<BuilderQueryEditorReduceExpression>>) => { |
||||
setAggregates(newItems); |
||||
|
||||
buildAndUpdateQuery({ |
||||
reduce: newItems, |
||||
}); |
||||
}; |
||||
|
||||
const onDeleteAggregate = (aggregateToDelete: BuilderQueryEditorReduceExpression) => { |
||||
setAggregates((prevAggregates) => { |
||||
const updatedAggregates = prevAggregates.filter( |
||||
(agg) => |
||||
agg.property?.name !== aggregateToDelete.property?.name || agg.reduce?.name !== aggregateToDelete.reduce?.name |
||||
); |
||||
|
||||
buildAndUpdateQuery({ |
||||
reduce: updatedAggregates.length === 0 ? [] : updatedAggregates, |
||||
}); |
||||
|
||||
return updatedAggregates; |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<div data-testid="aggregate-section"> |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField |
||||
label="Aggregate" |
||||
optional={true} |
||||
tooltip={`Perform calculations across rows of data, such as count, sum, average, minimum, maximum, standard deviation or percentiles.`} |
||||
> |
||||
<EditorList |
||||
items={aggregates} |
||||
onChange={onChange} |
||||
renderItem={makeRenderAggregate(availableColumns, onDeleteAggregate, templateVariableOptions)} |
||||
/> |
||||
</EditorField> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
function makeRenderAggregate( |
||||
availableColumns: Array<SelectableValue<string>>, |
||||
onDeleteAggregate: (aggregate: BuilderQueryEditorReduceExpression) => void, |
||||
templateVariableOptions: SelectableValue<string> |
||||
) { |
||||
return function renderAggregate( |
||||
item: BuilderQueryEditorReduceExpression, |
||||
onChange: (item: BuilderQueryEditorReduceExpression) => void |
||||
) { |
||||
return ( |
||||
<AggregateItem |
||||
aggregate={item} |
||||
onChange={onChange} |
||||
onDelete={() => onDeleteAggregate(item)} |
||||
columns={availableColumns} |
||||
templateVariableOptions={templateVariableOptions} |
||||
/> |
||||
); |
||||
}; |
||||
} |
@ -0,0 +1,197 @@ |
||||
import { BuilderQueryEditorExpressionType } from '../../dataquery.gen'; |
||||
|
||||
import { AzureMonitorKustoQueryBuilder } from './AzureMonitorKustoQueryBuilder'; |
||||
|
||||
describe('AzureMonitorKustoQueryParser', () => { |
||||
it('returns empty string if from table is not specified', () => { |
||||
const builderQuery: any = { from: { property: { name: '' } } }; |
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toBe(''); |
||||
}); |
||||
|
||||
it('builds a query with table and project', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
columns: { columns: ['TimeGenerated', 'Level', 'Message'] }, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain('Logs'); |
||||
expect(result).toContain('project TimeGenerated, Level, Message'); |
||||
}); |
||||
|
||||
it('includes time filter when needed', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
timeFilter: { |
||||
expressions: [ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
operator: { name: '$__timeFilter' }, |
||||
property: { name: 'TimeGenerated' }, |
||||
}, |
||||
], |
||||
}, |
||||
columns: { columns: ['TimeGenerated', 'Level'] }, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain('$__timeFilter(TimeGenerated)'); |
||||
}); |
||||
|
||||
it('handles fuzzy search expressions', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
fuzzySearch: { |
||||
expressions: [ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
operator: { name: 'contains', value: 'fail' }, |
||||
property: { name: 'Message' }, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain("Message contains 'fail'"); |
||||
}); |
||||
|
||||
it('applies additional filters', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
where: { |
||||
expressions: [ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
operator: { name: '==', value: 'Error' }, |
||||
property: { name: 'Level' }, |
||||
}, |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
operator: { name: 'contains', value: 'fail' }, |
||||
property: { name: 'Message' }, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain("Level == 'Error'"); |
||||
expect(result).toContain("Message contains 'fail'"); |
||||
}); |
||||
|
||||
it('handles where expressions with operator', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
columns: { columns: ['Level', 'Message'] }, |
||||
where: { |
||||
expressions: [ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
operator: { name: '==', value: 'Error' }, |
||||
property: { name: 'Level' }, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain("Level == 'Error'"); |
||||
}); |
||||
|
||||
it('handles summarize with percentile function', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
reduce: { |
||||
expressions: [ |
||||
{ |
||||
reduce: { name: 'percentile' }, |
||||
parameters: [{ value: '95' }, { value: 'Duration' }], |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain('summarize percentile(95, Duration)'); |
||||
}); |
||||
|
||||
it('handles summarize with basic aggregation function like avg', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
reduce: { |
||||
expressions: [ |
||||
{ |
||||
reduce: { name: 'avg' }, |
||||
property: { name: 'ResponseTime' }, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain('summarize avg(ResponseTime)'); |
||||
}); |
||||
|
||||
it('skips summarize when reduce expressions are invalid', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
reduce: { |
||||
expressions: [ |
||||
{ |
||||
reduce: null, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).not.toContain('summarize'); |
||||
}); |
||||
|
||||
it('adds summarize with groupBy', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
columns: { columns: ['Level'] }, |
||||
groupBy: { |
||||
expressions: [{ property: { name: 'Level' } }], |
||||
}, |
||||
reduce: { |
||||
expressions: [ |
||||
{ |
||||
reduce: { name: 'count' }, |
||||
property: { name: 'Level' }, |
||||
}, |
||||
], |
||||
}, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain('summarize count() by Level'); |
||||
}); |
||||
|
||||
it('adds order by clause', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
columns: { columns: ['TimeGenerated', 'Level'] }, |
||||
orderBy: { |
||||
expressions: [{ property: { name: 'TimeGenerated' }, order: 'desc' }], |
||||
}, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain('order by TimeGenerated desc'); |
||||
}); |
||||
|
||||
it('adds limit clause', () => { |
||||
const builderQuery: any = { |
||||
from: { property: { name: 'Logs' } }, |
||||
columns: { columns: ['TimeGenerated', 'Level'] }, |
||||
limit: 50, |
||||
}; |
||||
|
||||
const result = AzureMonitorKustoQueryBuilder.toQuery(builderQuery); |
||||
expect(result).toContain('limit 50'); |
||||
}); |
||||
}); |
@ -0,0 +1,147 @@ |
||||
import { |
||||
BuilderQueryEditorWhereExpression, |
||||
BuilderQueryEditorWhereExpressionArray, |
||||
BuilderQueryEditorWhereExpressionItems, |
||||
BuilderQueryExpression, |
||||
} from '../../dataquery.gen'; |
||||
|
||||
const isNestedExpression = ( |
||||
exp: BuilderQueryEditorWhereExpression | BuilderQueryEditorWhereExpressionItems |
||||
): exp is BuilderQueryEditorWhereExpressionItems => |
||||
'operator' in exp && |
||||
'property' in exp && |
||||
typeof exp.operator?.name === 'string' && |
||||
typeof exp.property?.name === 'string'; |
||||
|
||||
const buildCondition = ( |
||||
exp: BuilderQueryEditorWhereExpression | BuilderQueryEditorWhereExpressionItems |
||||
): string | undefined => { |
||||
if ('expressions' in exp && Array.isArray(exp.expressions)) { |
||||
const isGroupOfFilters = exp.expressions.every((e) => 'operator' in e && 'property' in e); |
||||
|
||||
const nested = exp.expressions.map(buildCondition).filter((c): c is string => Boolean(c)); |
||||
|
||||
if (nested.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
const joiner = isGroupOfFilters ? ' or ' : ' and '; |
||||
const joined = nested.join(joiner); |
||||
|
||||
return nested.length > 1 ? `(${joined})` : joined; |
||||
} |
||||
|
||||
if (isNestedExpression(exp)) { |
||||
const { name: op, value } = exp.operator; |
||||
const { name: prop } = exp.property; |
||||
const escapedValue = String(value).replace(/'/g, "''"); |
||||
return op === '$__timeFilter' ? `$__timeFilter(${prop})` : `${prop} ${op} '${escapedValue}'`; |
||||
} |
||||
|
||||
return; |
||||
}; |
||||
|
||||
export const appendWhere = ( |
||||
phrases: string[], |
||||
timeFilter?: BuilderQueryEditorWhereExpressionArray, |
||||
fuzzySearch?: BuilderQueryEditorWhereExpressionArray, |
||||
where?: BuilderQueryEditorWhereExpressionArray |
||||
): void => { |
||||
const groups = [timeFilter, fuzzySearch, where]; |
||||
|
||||
groups.forEach((group) => { |
||||
group?.expressions.forEach((exp) => { |
||||
const condition = buildCondition(exp); |
||||
if (condition) { |
||||
phrases.push(`where ${condition}`); |
||||
} |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
const appendProject = (builderQuery: BuilderQueryExpression, phrases: string[]) => { |
||||
const selectedColumns = builderQuery.columns?.columns || []; |
||||
if (selectedColumns.length > 0) { |
||||
phrases.push(`project ${selectedColumns.join(', ')}`); |
||||
} |
||||
}; |
||||
|
||||
const appendSummarize = (builderQuery: BuilderQueryExpression, phrases: string[]) => { |
||||
const summarizeAlreadyAdded = phrases.some((phrase) => phrase.startsWith('summarize')); |
||||
if (summarizeAlreadyAdded) { |
||||
return; |
||||
} |
||||
|
||||
const reduceExprs = builderQuery.reduce?.expressions ?? []; |
||||
const groupBy = builderQuery.groupBy?.expressions?.map((exp) => exp.property?.name).filter(Boolean) ?? []; |
||||
|
||||
const summarizeParts = reduceExprs |
||||
.map((expr) => { |
||||
if (!expr.reduce?.name) { |
||||
return; |
||||
} |
||||
|
||||
const func = expr.reduce.name; |
||||
|
||||
if (func === 'percentile') { |
||||
const percentileValue = expr.parameters?.[0]?.value; |
||||
const column = expr.parameters?.[1]?.value ?? expr.property?.name ?? ''; |
||||
return column ? `percentile(${percentileValue}, ${column})` : null; |
||||
} |
||||
|
||||
const column = expr.property?.name ?? ''; |
||||
return func === 'count' ? 'count()' : column ? `${func}(${column})` : func; |
||||
}) |
||||
.filter(Boolean); |
||||
|
||||
if (summarizeParts.length === 0 && groupBy.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
const summarizeClause = |
||||
summarizeParts.length > 0 |
||||
? `summarize ${summarizeParts.join(', ')}${groupBy.length > 0 ? ` by ${groupBy.join(', ')}` : ''}` |
||||
: `summarize by ${groupBy.join(', ')}`; |
||||
|
||||
phrases.push(summarizeClause); |
||||
}; |
||||
|
||||
const appendOrderBy = (builderQuery: BuilderQueryExpression, phrases: string[]) => { |
||||
const orderBy = builderQuery.orderBy?.expressions || []; |
||||
if (!orderBy.length) { |
||||
return; |
||||
} |
||||
|
||||
const clauses = orderBy.map((order) => `${order.property?.name} ${order.order}`).filter(Boolean); |
||||
if (clauses.length > 0) { |
||||
phrases.push(`order by ${clauses.join(', ')}`); |
||||
} |
||||
}; |
||||
|
||||
const appendLimit = (builderQuery: BuilderQueryExpression, phrases: string[]) => { |
||||
if (builderQuery.limit && builderQuery.limit > 0) { |
||||
phrases.push(`limit ${builderQuery.limit}`); |
||||
} |
||||
}; |
||||
|
||||
const toQuery = (builderQuery: BuilderQueryExpression): string => { |
||||
const { from, timeFilter, fuzzySearch, where } = builderQuery; |
||||
if (!from?.property?.name) { |
||||
return ''; |
||||
} |
||||
|
||||
const phrases: string[] = []; |
||||
phrases.push(from.property.name); |
||||
|
||||
appendWhere(phrases, timeFilter, fuzzySearch, where); |
||||
appendProject(builderQuery, phrases); |
||||
appendSummarize(builderQuery, phrases); |
||||
appendOrderBy(builderQuery, phrases); |
||||
appendLimit(builderQuery, phrases); |
||||
|
||||
return phrases.join('\n| '); |
||||
}; |
||||
|
||||
export const AzureMonitorKustoQueryBuilder = { |
||||
toQuery, |
||||
}; |
@ -0,0 +1,71 @@ |
||||
import React from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { Button, Combobox, ComboboxOption, Label, Select } from '@grafana/ui'; |
||||
|
||||
import { BuilderQueryEditorWhereExpressionItems } from '../../dataquery.gen'; |
||||
|
||||
import { inputFieldSize, toOperatorOptions, valueToDefinition } from './utils'; |
||||
|
||||
interface FilterItemProps { |
||||
filter: BuilderQueryEditorWhereExpressionItems; |
||||
filterIndex: number; |
||||
groupIndex: number; |
||||
usedColumns: string[]; |
||||
selectableOptions: Array<SelectableValue<string>>; |
||||
onChange: (groupIndex: number, field: 'property' | 'operator' | 'value', value: string, filterIndex: number) => void; |
||||
onDelete: (groupIndex: number, filterIndex: number) => void; |
||||
getFilterValues: ( |
||||
filter: BuilderQueryEditorWhereExpressionItems, |
||||
inputValue: string |
||||
) => Promise<Array<ComboboxOption<string>>>; |
||||
showOr: boolean; |
||||
} |
||||
|
||||
export const FilterItem: React.FC<FilterItemProps> = ({ |
||||
filter, |
||||
filterIndex, |
||||
groupIndex, |
||||
usedColumns, |
||||
selectableOptions, |
||||
onChange, |
||||
onDelete, |
||||
getFilterValues, |
||||
showOr, |
||||
}) => { |
||||
return ( |
||||
<div style={{ display: 'flex', alignItems: 'center' }}> |
||||
<Select |
||||
aria-label="column" |
||||
width={inputFieldSize} |
||||
value={valueToDefinition(filter.property.name)} |
||||
options={selectableOptions.filter((opt) => !usedColumns.includes(opt.value!))} |
||||
onChange={(e) => e.value && onChange(groupIndex, 'property', e.value, filterIndex)} |
||||
/> |
||||
<Select |
||||
aria-label="operator" |
||||
width={12} |
||||
value={{ label: filter.operator.name, value: filter.operator.name }} |
||||
options={toOperatorOptions('string')} |
||||
onChange={(e) => e.value && onChange(groupIndex, 'operator', e.value, filterIndex)} |
||||
/> |
||||
<Combobox |
||||
aria-label="column value" |
||||
value={ |
||||
filter.operator.value |
||||
? { |
||||
label: String(filter.operator.value), |
||||
value: String(filter.operator.value), |
||||
} |
||||
: null |
||||
} |
||||
options={(inputValue: string) => getFilterValues(filter, inputValue)} |
||||
onChange={(e) => e.value && onChange(groupIndex, 'value', String(e.value), filterIndex)} |
||||
width={inputFieldSize} |
||||
disabled={!filter.property?.name} |
||||
/> |
||||
<Button variant="secondary" icon="times" onClick={() => onDelete(groupIndex, filterIndex)} /> |
||||
{showOr && <Label style={{ padding: '9px 14px' }}>OR</Label>} |
||||
</div> |
||||
); |
||||
}; |
@ -0,0 +1,260 @@ |
||||
import { css } from '@emotion/css'; |
||||
import React, { useEffect, useRef, useState } from 'react'; |
||||
import { lastValueFrom } from 'rxjs'; |
||||
|
||||
import { CoreApp, getDefaultTimeRange, SelectableValue, TimeRange } from '@grafana/data'; |
||||
import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui'; |
||||
import { Button, ComboboxOption, Label, useStyles2 } from '@grafana/ui'; |
||||
|
||||
import { |
||||
AzureQueryType, |
||||
BuilderQueryEditorExpressionType, |
||||
BuilderQueryEditorPropertyType, |
||||
BuilderQueryEditorWhereExpression, |
||||
BuilderQueryEditorWhereExpressionItems, |
||||
} from '../../dataquery.gen'; |
||||
import Datasource from '../../datasource'; |
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; |
||||
|
||||
import { FilterItem } from './FilterItem'; |
||||
import { BuildAndUpdateOptions } from './utils'; |
||||
|
||||
interface FilterSectionProps { |
||||
query: AzureMonitorQuery; |
||||
allColumns: AzureLogAnalyticsMetadataColumn[]; |
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void; |
||||
templateVariableOptions: SelectableValue<string>; |
||||
datasource: Datasource; |
||||
timeRange?: TimeRange; |
||||
} |
||||
|
||||
const filterDynamicColumns = (columns: string[], allColumns: AzureLogAnalyticsMetadataColumn[]) => { |
||||
return columns.filter((col) => |
||||
allColumns.some((completeCol) => completeCol.name === col && completeCol.type !== 'dynamic') |
||||
); |
||||
}; |
||||
|
||||
export const FilterSection: React.FC<FilterSectionProps> = ({ |
||||
buildAndUpdateQuery, |
||||
query, |
||||
allColumns, |
||||
templateVariableOptions, |
||||
datasource, |
||||
timeRange, |
||||
}) => { |
||||
const styles = useStyles2(() => ({ filters: css({ marginBottom: '8px' }) })); |
||||
const builderQuery = query.azureLogAnalytics?.builderQuery; |
||||
|
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null); |
||||
const [filters, setFilters] = useState<BuilderQueryEditorWhereExpression[]>( |
||||
builderQuery?.where?.expressions?.map((group) => ({ |
||||
...group, |
||||
expressions: group.expressions ?? [], |
||||
})) || [] |
||||
); |
||||
const hasLoadedFilters = useRef(false); |
||||
|
||||
const variableOptions = Array.isArray(templateVariableOptions) ? templateVariableOptions : [templateVariableOptions]; |
||||
|
||||
const availableColumns: Array<SelectableValue<string>> = builderQuery?.columns?.columns?.length |
||||
? filterDynamicColumns(builderQuery.columns.columns, allColumns).map((col) => ({ label: col, value: col })) |
||||
: allColumns.filter((col) => col.type !== 'dynamic').map((col) => ({ label: col.name, value: col.name })); |
||||
|
||||
const selectableOptions = [...availableColumns, ...variableOptions]; |
||||
|
||||
const usedColumnsInOtherGroups = (currentGroupIndex: number): string[] => { |
||||
return filters |
||||
.flatMap((group, idx) => (idx !== currentGroupIndex ? group.expressions : [])) |
||||
.map((exp) => exp.property.name) |
||||
.filter(Boolean); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
const currentTable = builderQuery?.from?.property.name || null; |
||||
if (prevTable.current !== currentTable || builderQuery?.where?.expressions.length === 0) { |
||||
setFilters([]); |
||||
hasLoadedFilters.current = false; |
||||
prevTable.current = currentTable; |
||||
} |
||||
}, [builderQuery]); |
||||
|
||||
const updateFilters = (updated: BuilderQueryEditorWhereExpression[]) => { |
||||
setFilters(updated); |
||||
buildAndUpdateQuery({ where: updated }); |
||||
}; |
||||
|
||||
const onAddOrFilters = ( |
||||
groupIndex: number, |
||||
field: 'property' | 'operator' | 'value', |
||||
value: string, |
||||
filterIndex?: number |
||||
) => { |
||||
const updated = [...filters]; |
||||
const group = updated[groupIndex]; |
||||
if (!group) { |
||||
return; |
||||
} |
||||
|
||||
let filter: BuilderQueryEditorWhereExpressionItems = |
||||
filterIndex !== undefined |
||||
? { ...group.expressions[filterIndex] } |
||||
: { |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
property: { name: '', type: BuilderQueryEditorPropertyType.String }, |
||||
operator: { name: '==', value: '' }, |
||||
}; |
||||
|
||||
if (field === 'property') { |
||||
filter.property.name = value; |
||||
filter.operator.value = ''; |
||||
} else if (field === 'operator') { |
||||
filter.operator.name = value; |
||||
} else if (field === 'value') { |
||||
filter.operator.value = value; |
||||
} |
||||
|
||||
const isValid = filter.property.name && filter.operator.name && filter.operator.value !== ''; |
||||
|
||||
if (filterIndex !== undefined) { |
||||
group.expressions[filterIndex] = filter; |
||||
} else { |
||||
group.expressions.push(filter); |
||||
} |
||||
|
||||
updated[groupIndex] = group; |
||||
setFilters(updated); |
||||
if (isValid) { |
||||
updateFilters(updated); |
||||
} |
||||
}; |
||||
|
||||
const onAddAndFilters = () => { |
||||
const updated = [ |
||||
...filters, |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Or, |
||||
expressions: [ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
property: { name: '', type: BuilderQueryEditorPropertyType.String }, |
||||
operator: { name: '==', value: '' }, |
||||
}, |
||||
], |
||||
}, |
||||
]; |
||||
updateFilters(updated); |
||||
}; |
||||
|
||||
const onDeleteFilter = (groupIndex: number, filterIndex: number) => { |
||||
const updated = [...filters]; |
||||
updated[groupIndex].expressions.splice(filterIndex, 1); |
||||
if (updated[groupIndex].expressions.length === 0) { |
||||
updated.splice(groupIndex, 1); |
||||
} |
||||
updateFilters(updated); |
||||
}; |
||||
|
||||
const getFilterValues = async (filter: BuilderQueryEditorWhereExpressionItems) => { |
||||
const from = timeRange?.from?.toISOString(); |
||||
const to = timeRange?.to?.toISOString(); |
||||
const timeColumn = query.azureLogAnalytics?.timeColumn || 'TimeGenerated'; |
||||
|
||||
const kustoQuery = ` |
||||
${query.azureLogAnalytics?.builderQuery?.from?.property.name} |
||||
| where ${timeColumn} >= datetime(${from}) and ${timeColumn} <= datetime(${to}) |
||||
| distinct ${filter.property.name} |
||||
| limit 1000 |
||||
`;
|
||||
|
||||
const results: any = await lastValueFrom( |
||||
datasource.azureLogAnalyticsDatasource.query({ |
||||
requestId: 'azure-logs-builder-filter-values', |
||||
interval: '', |
||||
intervalMs: 0, |
||||
scopedVars: {}, |
||||
timezone: '', |
||||
app: CoreApp.Unknown, |
||||
startTime: 0, |
||||
range: timeRange || getDefaultTimeRange(), |
||||
targets: [ |
||||
{ |
||||
refId: 'A', |
||||
queryType: AzureQueryType.LogAnalytics, |
||||
azureLogAnalytics: { |
||||
query: kustoQuery, |
||||
resources: query.azureLogAnalytics?.resources ?? [], |
||||
}, |
||||
}, |
||||
], |
||||
}) |
||||
); |
||||
|
||||
if (results.state === 'Done') { |
||||
const values = results.data?.[0]?.fields?.[0]?.values ?? []; |
||||
|
||||
return values.toArray().map( |
||||
(v: any): ComboboxOption<string> => ({ |
||||
label: String(v), |
||||
value: String(v), |
||||
}) |
||||
); |
||||
} |
||||
|
||||
return []; |
||||
}; |
||||
|
||||
return ( |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField label="Filters" optional tooltip="Narrow results by applying conditions to specific columns."> |
||||
<div className={styles.filters}> |
||||
{filters.length === 0 || filters.every((g) => g.expressions.length === 0) ? ( |
||||
<InputGroup> |
||||
<Button variant="secondary" onClick={onAddAndFilters} icon="plus" /> |
||||
</InputGroup> |
||||
) : ( |
||||
<> |
||||
{filters.map((group, groupIndex) => ( |
||||
<div key={groupIndex}> |
||||
{groupIndex > 0 && filters[groupIndex - 1]?.expressions.length > 0 && ( |
||||
<Label style={{ padding: '9px 14px' }}>AND</Label> |
||||
)} |
||||
<InputGroup> |
||||
<> |
||||
{group.expressions.map((filter, filterIndex) => ( |
||||
<FilterItem |
||||
key={`${groupIndex}-${filterIndex}`} |
||||
filter={filter} |
||||
filterIndex={filterIndex} |
||||
groupIndex={groupIndex} |
||||
usedColumns={usedColumnsInOtherGroups(groupIndex)} |
||||
selectableOptions={selectableOptions} |
||||
onChange={onAddOrFilters} |
||||
onDelete={onDeleteFilter} |
||||
getFilterValues={getFilterValues} |
||||
showOr={filterIndex < group.expressions.length - 1} |
||||
/> |
||||
))} |
||||
</> |
||||
<Button |
||||
variant="secondary" |
||||
style={{ marginLeft: '15px' }} |
||||
onClick={() => onAddOrFilters(groupIndex, 'property', '')} |
||||
icon="plus" |
||||
/> |
||||
</InputGroup> |
||||
</div> |
||||
))} |
||||
{filters.some((g) => g.expressions.length > 0) && ( |
||||
<Button variant="secondary" onClick={onAddAndFilters} style={{ marginTop: '8px' }}> |
||||
Add group |
||||
</Button> |
||||
)} |
||||
</> |
||||
)} |
||||
</div> |
||||
</EditorField> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
); |
||||
}; |
@ -0,0 +1,135 @@ |
||||
import React, { useState, useEffect, useRef } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorRow, EditorFieldGroup, EditorField, InputGroup } from '@grafana/plugin-ui'; |
||||
import { Button, Input, Select } from '@grafana/ui'; |
||||
|
||||
import { |
||||
BuilderQueryEditorExpressionType, |
||||
BuilderQueryEditorWhereExpression, |
||||
BuilderQueryEditorPropertyType, |
||||
} from '../../dataquery.gen'; |
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; |
||||
|
||||
import { BuildAndUpdateOptions, removeExtraQuotes } from './utils'; |
||||
|
||||
interface FuzzySearchProps { |
||||
query: AzureMonitorQuery; |
||||
allColumns: AzureLogAnalyticsMetadataColumn[]; |
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void; |
||||
templateVariableOptions: SelectableValue<string>; |
||||
} |
||||
|
||||
export const FuzzySearch: React.FC<FuzzySearchProps> = ({ |
||||
buildAndUpdateQuery, |
||||
query, |
||||
allColumns, |
||||
templateVariableOptions, |
||||
}) => { |
||||
const builderQuery = query.azureLogAnalytics?.builderQuery; |
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null); |
||||
|
||||
const [searchTerm, setSearchTerm] = useState<string>(''); |
||||
const [selectedColumn, setSelectedColumn] = useState<string>(''); |
||||
const [isOpen, setIsOpen] = useState<boolean>(false); |
||||
const hasLoadedFuzzySearch = useRef(false); |
||||
|
||||
useEffect(() => { |
||||
const currentTable = builderQuery?.from?.property.name || null; |
||||
|
||||
if (prevTable.current !== currentTable) { |
||||
setSearchTerm(''); |
||||
setSelectedColumn(''); |
||||
setIsOpen(false); |
||||
hasLoadedFuzzySearch.current = false; |
||||
prevTable.current = currentTable; |
||||
} |
||||
|
||||
if (!hasLoadedFuzzySearch.current && builderQuery?.fuzzySearch?.expressions?.length) { |
||||
const fuzzy = builderQuery.fuzzySearch.expressions[0]; |
||||
setSearchTerm(removeExtraQuotes(String(fuzzy.expressions[0].operator?.value ?? ''))); |
||||
setSelectedColumn(fuzzy.expressions[0].property?.name ?? '*'); |
||||
setIsOpen(true); |
||||
hasLoadedFuzzySearch.current = true; |
||||
} |
||||
}, [builderQuery]); |
||||
|
||||
const columnOptions: Array<SelectableValue<string>> = allColumns.map((col) => ({ |
||||
label: col.name, |
||||
value: col.name, |
||||
})); |
||||
|
||||
const safeTemplateVariables: Array<SelectableValue<string>> = Array.isArray(templateVariableOptions) |
||||
? templateVariableOptions |
||||
: [templateVariableOptions]; |
||||
|
||||
const defaultColumn: SelectableValue<string> = { label: 'All Columns *', value: '*' }; |
||||
const selectableOptions = [defaultColumn, ...columnOptions, ...safeTemplateVariables]; |
||||
|
||||
const updateFuzzySearch = (column: string, term: string) => { |
||||
setSearchTerm(term); |
||||
setSelectedColumn(column); |
||||
|
||||
const fuzzyExpression: BuilderQueryEditorWhereExpression = { |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
expressions: [ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Property, |
||||
operator: { name: 'has', value: term }, |
||||
property: { name: column || '*', type: BuilderQueryEditorPropertyType.String }, |
||||
}, |
||||
], |
||||
}; |
||||
|
||||
buildAndUpdateQuery({ |
||||
fuzzySearch: term ? [fuzzyExpression] : [], |
||||
}); |
||||
}; |
||||
|
||||
const onDeleteFuzzySearch = () => { |
||||
setSearchTerm(''); |
||||
setSelectedColumn(''); |
||||
setIsOpen(false); |
||||
|
||||
buildAndUpdateQuery({ |
||||
fuzzySearch: [], |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField |
||||
label="Fuzzy Search" |
||||
optional={true} |
||||
tooltip={`Find approximate text matches with tolerance for spelling variations. By default, fuzzy search scans all
|
||||
columns (*) in the entire table, not just specific fields.`}
|
||||
> |
||||
<InputGroup> |
||||
{isOpen ? ( |
||||
<> |
||||
<Input |
||||
className="width-10" |
||||
type="text" |
||||
placeholder="Enter search term" |
||||
value={searchTerm} |
||||
onChange={(e) => updateFuzzySearch(selectedColumn, e.currentTarget.value)} |
||||
/> |
||||
<Select |
||||
aria-label="Select Column" |
||||
options={selectableOptions} |
||||
value={{ label: selectedColumn || '*', value: selectedColumn || '*' }} |
||||
onChange={(e: SelectableValue<string>) => updateFuzzySearch(e.value ?? '*', searchTerm)} |
||||
width="auto" |
||||
/> |
||||
<Button variant="secondary" icon="times" onClick={onDeleteFuzzySearch} /> |
||||
</> |
||||
) : ( |
||||
<Button variant="secondary" onClick={() => setIsOpen(true)} icon="plus" /> |
||||
)} |
||||
</InputGroup> |
||||
</EditorField> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
); |
||||
}; |
@ -0,0 +1,78 @@ |
||||
import React from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { AccessoryButton, InputGroup } from '@grafana/plugin-ui'; |
||||
import { Select } from '@grafana/ui'; |
||||
|
||||
import { |
||||
BuilderQueryEditorGroupByExpression, |
||||
BuilderQueryEditorPropertyType, |
||||
BuilderQueryEditorExpressionType, |
||||
} from '../../dataquery.gen'; |
||||
|
||||
import { inputFieldSize } from './utils'; |
||||
|
||||
interface GroupByItemProps { |
||||
groupBy: BuilderQueryEditorGroupByExpression; |
||||
columns: Array<SelectableValue<string>>; |
||||
onChange: (item: BuilderQueryEditorGroupByExpression) => void; |
||||
onDelete: () => void; |
||||
templateVariableOptions: SelectableValue<string>; |
||||
} |
||||
|
||||
export const GroupByItem: React.FC<GroupByItemProps> = ({ |
||||
groupBy, |
||||
onChange, |
||||
onDelete, |
||||
columns, |
||||
templateVariableOptions, |
||||
}) => { |
||||
const columnOptions: Array<SelectableValue<string>> = |
||||
columns.length > 0 |
||||
? columns.map((c) => ({ label: c.label, value: c.value })) |
||||
: [{ label: 'No columns available', value: '' }]; |
||||
|
||||
const selectableOptions: Array<SelectableValue<string>> = [ |
||||
...columnOptions, |
||||
...(templateVariableOptions |
||||
? Array.isArray(templateVariableOptions) |
||||
? templateVariableOptions |
||||
: [templateVariableOptions] |
||||
: []), |
||||
]; |
||||
|
||||
const handleChange = (selectedValue: SelectableValue<string>) => { |
||||
if (!selectedValue.value) { |
||||
return; |
||||
} |
||||
|
||||
const isTemplateVariable = selectedValue.value.startsWith('$'); |
||||
const selectedColumn = columns.find((c) => c.value === selectedValue.value); |
||||
|
||||
onChange({ |
||||
...groupBy, |
||||
property: { |
||||
name: selectedValue.value, |
||||
type: isTemplateVariable |
||||
? BuilderQueryEditorPropertyType.String |
||||
: selectedColumn?.type || BuilderQueryEditorPropertyType.String, |
||||
}, |
||||
interval: groupBy.interval, |
||||
type: BuilderQueryEditorExpressionType.Group_by, |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<InputGroup> |
||||
<Select |
||||
aria-label="column" |
||||
width={inputFieldSize} |
||||
value={groupBy.property?.name ? { label: groupBy.property.name, value: groupBy.property.name } : null} |
||||
options={selectableOptions} |
||||
allowCustomValue |
||||
onChange={handleChange} |
||||
/> |
||||
<AccessoryButton aria-label="remove" icon="times" variant="secondary" onClick={onDelete} /> |
||||
</InputGroup> |
||||
); |
||||
}; |
@ -0,0 +1,150 @@ |
||||
import React, { useEffect, useState, useRef } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorField, EditorFieldGroup, EditorList, EditorRow, InputGroup } from '@grafana/plugin-ui'; |
||||
import { Button } from '@grafana/ui'; |
||||
|
||||
import { |
||||
BuilderQueryEditorExpressionType, |
||||
BuilderQueryEditorGroupByExpression, |
||||
BuilderQueryEditorPropertyType, |
||||
} from '../../dataquery.gen'; |
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; |
||||
|
||||
import { GroupByItem } from './GroupByItem'; |
||||
import { BuildAndUpdateOptions } from './utils'; |
||||
|
||||
interface GroupBySectionProps { |
||||
query: AzureMonitorQuery; |
||||
allColumns: AzureLogAnalyticsMetadataColumn[]; |
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void; |
||||
templateVariableOptions: SelectableValue<string>; |
||||
} |
||||
|
||||
export const GroupBySection: React.FC<GroupBySectionProps> = ({ |
||||
query, |
||||
buildAndUpdateQuery, |
||||
allColumns, |
||||
templateVariableOptions, |
||||
}) => { |
||||
const builderQuery = query.azureLogAnalytics?.builderQuery; |
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null); |
||||
const [groupBys, setGroupBys] = useState<BuilderQueryEditorGroupByExpression[]>( |
||||
builderQuery?.groupBy?.expressions || [] |
||||
); |
||||
|
||||
const hasLoadedGroupBy = useRef(false); |
||||
|
||||
useEffect(() => { |
||||
const currentTable = builderQuery?.from?.property.name || null; |
||||
|
||||
if (prevTable.current !== currentTable || builderQuery?.groupBy?.expressions.length === 0) { |
||||
setGroupBys([]); |
||||
hasLoadedGroupBy.current = false; |
||||
prevTable.current = currentTable; |
||||
} |
||||
}, [builderQuery]); |
||||
|
||||
const availableColumns: Array<SelectableValue<string>> = []; |
||||
const columns = builderQuery?.columns?.columns ?? []; |
||||
|
||||
if (columns.length > 0) { |
||||
availableColumns.push( |
||||
...columns.map((col) => ({ |
||||
label: col, |
||||
value: col, |
||||
})) |
||||
); |
||||
} else { |
||||
availableColumns.push( |
||||
...allColumns.map((col) => ({ |
||||
label: col.name, |
||||
value: col.name, |
||||
})) |
||||
); |
||||
} |
||||
|
||||
const handleGroupByChange = (newItems: Array<Partial<BuilderQueryEditorGroupByExpression>>) => { |
||||
setGroupBys(newItems); |
||||
|
||||
buildAndUpdateQuery({ |
||||
groupBy: newItems, |
||||
}); |
||||
}; |
||||
|
||||
const onDeleteGroupBy = (propertyName: string) => { |
||||
setGroupBys((prevGroupBys) => { |
||||
const updatedGroupBys = prevGroupBys.filter((gb) => gb.property?.name !== propertyName); |
||||
|
||||
buildAndUpdateQuery({ |
||||
groupBy: updatedGroupBys, |
||||
}); |
||||
|
||||
return updatedGroupBys; |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField |
||||
label="Group by" |
||||
optional={true} |
||||
tooltip={`Organize results into categories based on specified columns. Group by can be used independently to list
|
||||
unique values in selected columns, or combined with aggregate functions to produce summary statistics for |
||||
each group. When used alone, it returns distinct combinations of the specified columns.`}
|
||||
> |
||||
<InputGroup> |
||||
{groupBys.length > 0 ? ( |
||||
<EditorList |
||||
items={groupBys} |
||||
onChange={handleGroupByChange} |
||||
renderItem={makeRenderGroupBy(availableColumns, onDeleteGroupBy, templateVariableOptions)} |
||||
/> |
||||
) : ( |
||||
<Button |
||||
variant="secondary" |
||||
icon="plus" |
||||
onClick={() => |
||||
handleGroupByChange([ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Group_by, |
||||
property: { type: BuilderQueryEditorPropertyType.String, name: '' }, |
||||
}, |
||||
]) |
||||
} |
||||
/> |
||||
)} |
||||
</InputGroup> |
||||
</EditorField> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
); |
||||
}; |
||||
|
||||
const makeRenderGroupBy = ( |
||||
columns: Array<SelectableValue<string>>, |
||||
onDeleteGroupBy: (propertyName: string) => void, |
||||
templateVariableOptions: SelectableValue<string> |
||||
) => { |
||||
return ( |
||||
item: BuilderQueryEditorGroupByExpression, |
||||
onChangeItem: (updatedItem: BuilderQueryEditorGroupByExpression) => void, |
||||
onDeleteItem: () => void |
||||
) => ( |
||||
<GroupByItem |
||||
groupBy={item} |
||||
onChange={(updatedItem) => { |
||||
onChangeItem(updatedItem); |
||||
}} |
||||
onDelete={() => { |
||||
if (item.property?.name) { |
||||
onDeleteGroupBy(item.property.name); |
||||
} |
||||
onDeleteItem(); |
||||
}} |
||||
columns={columns} |
||||
templateVariableOptions={templateVariableOptions} |
||||
/> |
||||
); |
||||
}; |
@ -0,0 +1,61 @@ |
||||
import { css } from '@emotion/css'; |
||||
import Prism from 'prismjs'; |
||||
import React, { useEffect } from 'react'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/plugin-ui'; |
||||
import { Button, useStyles2 } from '@grafana/ui'; |
||||
|
||||
import 'prismjs/components/prism-kusto'; |
||||
import 'prismjs/themes/prism-tomorrow.min.css'; |
||||
|
||||
interface KQLPreviewProps { |
||||
query: string; |
||||
hidden: boolean; |
||||
setHidden: React.Dispatch<React.SetStateAction<boolean>>; |
||||
} |
||||
|
||||
const KQLPreview: React.FC<KQLPreviewProps> = ({ query, hidden, setHidden }) => { |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
useEffect(() => { |
||||
Prism.highlightAll(); |
||||
}, [query]); |
||||
|
||||
return ( |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField label="Query Preview"> |
||||
<> |
||||
<Button hidden={!hidden} variant="secondary" onClick={() => setHidden(false)} size="sm"> |
||||
show |
||||
</Button> |
||||
<div className={styles.codeBlock} hidden={hidden}> |
||||
<pre className={styles.code}> |
||||
<code className="language-kusto">{query}</code> |
||||
</pre> |
||||
</div> |
||||
<Button hidden={hidden} variant="secondary" onClick={() => setHidden(true)} size="sm"> |
||||
hide |
||||
</Button> |
||||
</> |
||||
</EditorField> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
); |
||||
}; |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => { |
||||
return { |
||||
codeBlock: css({ |
||||
width: '100%', |
||||
display: 'table', |
||||
tableLayout: 'fixed', |
||||
}), |
||||
code: css({ |
||||
marginBottom: '4px', |
||||
}), |
||||
}; |
||||
}; |
||||
|
||||
export default KQLPreview; |
@ -0,0 +1,41 @@ |
||||
import { useState } from 'react'; |
||||
|
||||
import { EditorRow, EditorFieldGroup, EditorField } from '@grafana/plugin-ui'; |
||||
import { Input } from '@grafana/ui'; |
||||
|
||||
import { BuildAndUpdateOptions } from './utils'; |
||||
|
||||
interface LimitSectionProps { |
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void; |
||||
} |
||||
|
||||
export const LimitSection: React.FC<LimitSectionProps> = (props) => { |
||||
const { buildAndUpdateQuery } = props; |
||||
const [limit, setLimit] = useState<number>(1000); |
||||
|
||||
const handleQueryLimitUpdate = (newLimit: number) => { |
||||
buildAndUpdateQuery({ |
||||
limit: newLimit, |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField label="Limit" optional={true} tooltip={`Restrict the number of rows returned (default is 1000).`}> |
||||
<Input |
||||
className="width-5" |
||||
type="number" |
||||
placeholder="Enter limit" |
||||
value={limit} |
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
||||
const newValue = e.target.value.replace(/[^0-9]/g, ''); |
||||
setLimit(Number(newValue)); |
||||
handleQueryLimitUpdate(Number(newValue)); |
||||
}} |
||||
/> |
||||
</EditorField> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
); |
||||
}; |
@ -0,0 +1,158 @@ |
||||
import React, { useMemo, useState, useCallback } from 'react'; |
||||
|
||||
import { SelectableValue, TimeRange } from '@grafana/data'; |
||||
import { EditorRows } from '@grafana/plugin-ui'; |
||||
import { Alert } from '@grafana/ui'; |
||||
|
||||
import { |
||||
BuilderQueryEditorExpressionType, |
||||
BuilderQueryEditorPropertyType, |
||||
BuilderQueryEditorReduceExpression, |
||||
BuilderQueryEditorWhereExpression, |
||||
BuilderQueryEditorGroupByExpression, |
||||
BuilderQueryEditorOrderByExpression, |
||||
BuilderQueryEditorPropertyExpression, |
||||
BuilderQueryExpression, |
||||
} from '../../dataquery.gen'; |
||||
import Datasource from '../../datasource'; |
||||
import { selectors } from '../../e2e/selectors'; |
||||
import { |
||||
AzureLogAnalyticsMetadataTable, |
||||
AzureLogAnalyticsMetadataColumn, |
||||
AzureMonitorQuery, |
||||
EngineSchema, |
||||
} from '../../types'; |
||||
|
||||
import { AggregateSection } from './AggregationSection'; |
||||
import { AzureMonitorKustoQueryBuilder } from './AzureMonitorKustoQueryBuilder'; |
||||
import { FilterSection } from './FilterSection'; |
||||
import { FuzzySearch } from './FuzzySearch'; |
||||
import { GroupBySection } from './GroupBySection'; |
||||
import KQLPreview from './KQLPreview'; |
||||
import { LimitSection } from './LimitSection'; |
||||
import { OrderBySection } from './OrderBySection'; |
||||
import { TableSection } from './TableSection'; |
||||
import { DEFAULT_LOGS_BUILDER_QUERY } from './utils'; |
||||
|
||||
interface LogsQueryBuilderProps { |
||||
query: AzureMonitorQuery; |
||||
basicLogsEnabled: boolean; |
||||
onQueryChange: (newQuery: AzureMonitorQuery) => void; |
||||
schema: EngineSchema; |
||||
templateVariableOptions: SelectableValue<string>; |
||||
datasource: Datasource; |
||||
timeRange?: TimeRange; |
||||
} |
||||
|
||||
export const LogsQueryBuilder: React.FC<LogsQueryBuilderProps> = (props) => { |
||||
const { query, onQueryChange, schema, datasource, timeRange } = props; |
||||
const [isKQLPreviewHidden, setIsKQLPreviewHidden] = useState<boolean>(true); |
||||
|
||||
const tables: AzureLogAnalyticsMetadataTable[] = useMemo(() => { |
||||
return schema?.database?.tables || []; |
||||
}, [schema?.database]); |
||||
|
||||
const builderQuery: BuilderQueryExpression = query.azureLogAnalytics?.builderQuery || DEFAULT_LOGS_BUILDER_QUERY; |
||||
|
||||
const allColumns: AzureLogAnalyticsMetadataColumn[] = useMemo(() => { |
||||
const tableName = builderQuery.from?.property.name; |
||||
const selectedTable = tables.find((table) => table.name === tableName); |
||||
return selectedTable?.columns || []; |
||||
}, [builderQuery, tables]); |
||||
|
||||
const buildAndUpdateQuery = useCallback( |
||||
({ |
||||
limit, |
||||
reduce, |
||||
where, |
||||
fuzzySearch, |
||||
groupBy, |
||||
orderBy, |
||||
columns, |
||||
from, |
||||
}: { |
||||
limit?: number; |
||||
reduce?: BuilderQueryEditorReduceExpression[]; |
||||
where?: BuilderQueryEditorWhereExpression[]; |
||||
fuzzySearch?: BuilderQueryEditorWhereExpression[]; |
||||
groupBy?: BuilderQueryEditorGroupByExpression[]; |
||||
orderBy?: BuilderQueryEditorOrderByExpression[]; |
||||
columns?: string[]; |
||||
from?: BuilderQueryEditorPropertyExpression; |
||||
}) => { |
||||
const datetimeColumn = allColumns.find((col) => col.type === 'datetime')?.name || 'TimeGenerated'; |
||||
|
||||
const timeFilterExpression: BuilderQueryEditorWhereExpression = { |
||||
type: BuilderQueryEditorExpressionType.Or, |
||||
expressions: [ |
||||
{ |
||||
type: BuilderQueryEditorExpressionType.Operator, |
||||
operator: { name: '$__timeFilter', value: datetimeColumn }, |
||||
property: { name: datetimeColumn, type: BuilderQueryEditorPropertyType.Datetime }, |
||||
}, |
||||
], |
||||
}; |
||||
|
||||
const updatedBuilderQuery: BuilderQueryExpression = { |
||||
...builderQuery, |
||||
...(limit !== undefined ? { limit } : {}), |
||||
...(reduce !== undefined |
||||
? { reduce: { expressions: reduce, type: BuilderQueryEditorExpressionType.Reduce } } |
||||
: {}), |
||||
...(where !== undefined ? { where: { expressions: where, type: BuilderQueryEditorExpressionType.And } } : {}), |
||||
...(fuzzySearch !== undefined |
||||
? { fuzzySearch: { expressions: fuzzySearch, type: BuilderQueryEditorExpressionType.And } } |
||||
: {}), |
||||
...(groupBy !== undefined |
||||
? { groupBy: { expressions: groupBy, type: BuilderQueryEditorExpressionType.Group_by } } |
||||
: {}), |
||||
...(orderBy !== undefined |
||||
? { orderBy: { expressions: orderBy, type: BuilderQueryEditorExpressionType.Order_by } } |
||||
: {}), |
||||
...(columns !== undefined ? { columns: { columns, type: BuilderQueryEditorExpressionType.Property } } : {}), |
||||
...(from !== undefined ? { from } : {}), |
||||
timeFilter: { expressions: [timeFilterExpression], type: BuilderQueryEditorExpressionType.And }, |
||||
}; |
||||
|
||||
const updatedQueryString = AzureMonitorKustoQueryBuilder.toQuery(updatedBuilderQuery); |
||||
|
||||
onQueryChange({ |
||||
...query, |
||||
azureLogAnalytics: { |
||||
...query.azureLogAnalytics, |
||||
builderQuery: updatedBuilderQuery, |
||||
query: updatedQueryString, |
||||
}, |
||||
}); |
||||
}, |
||||
[query, builderQuery, onQueryChange, allColumns] |
||||
); |
||||
|
||||
return ( |
||||
<span data-testid={selectors.components.queryEditor.logsQueryEditor.container.input}> |
||||
<EditorRows> |
||||
{schema && tables.length === 0 && ( |
||||
<Alert severity="warning" title="Resource loaded successfully but without any tables" /> |
||||
)} |
||||
<TableSection {...props} tables={tables} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} /> |
||||
<FilterSection |
||||
{...props} |
||||
allColumns={allColumns} |
||||
buildAndUpdateQuery={buildAndUpdateQuery} |
||||
datasource={datasource} |
||||
timeRange={timeRange} |
||||
/> |
||||
<AggregateSection {...props} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} /> |
||||
<GroupBySection {...props} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} /> |
||||
<OrderBySection {...props} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} /> |
||||
<FuzzySearch {...props} allColumns={allColumns} buildAndUpdateQuery={buildAndUpdateQuery} /> |
||||
<LimitSection {...props} buildAndUpdateQuery={buildAndUpdateQuery} /> |
||||
<KQLPreview |
||||
query={query.azureLogAnalytics?.query || ''} |
||||
hidden={isKQLPreviewHidden} |
||||
setHidden={setIsKQLPreviewHidden} |
||||
/> |
||||
</EditorRows> |
||||
</span> |
||||
); |
||||
}; |
@ -0,0 +1,158 @@ |
||||
import React, { useEffect, useRef, useState } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui'; |
||||
import { Button, Select, Label } from '@grafana/ui'; |
||||
|
||||
import { |
||||
BuilderQueryEditorExpressionType, |
||||
BuilderQueryEditorOrderByExpression, |
||||
BuilderQueryEditorOrderByOptions, |
||||
BuilderQueryEditorPropertyType, |
||||
} from '../../dataquery.gen'; |
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; |
||||
|
||||
import { BuildAndUpdateOptions, inputFieldSize } from './utils'; |
||||
|
||||
interface OrderBySectionProps { |
||||
query: AzureMonitorQuery; |
||||
allColumns: AzureLogAnalyticsMetadataColumn[]; |
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void; |
||||
} |
||||
|
||||
export const OrderBySection: React.FC<OrderBySectionProps> = ({ query, allColumns, buildAndUpdateQuery }) => { |
||||
const builderQuery = query.azureLogAnalytics?.builderQuery; |
||||
const prevTable = useRef<string | null>(builderQuery?.from?.property.name || null); |
||||
const hasLoadedOrderBy = useRef(false); |
||||
|
||||
const [orderBy, setOrderBy] = useState<BuilderQueryEditorOrderByExpression[]>( |
||||
builderQuery?.orderBy?.expressions || [] |
||||
); |
||||
|
||||
useEffect(() => { |
||||
const currentTable = builderQuery?.from?.property.name || null; |
||||
|
||||
if (prevTable.current !== currentTable || builderQuery?.orderBy?.expressions.length === 0) { |
||||
setOrderBy([]); |
||||
hasLoadedOrderBy.current = false; |
||||
prevTable.current = currentTable; |
||||
} |
||||
}, [builderQuery]); |
||||
|
||||
const groupByColumns = builderQuery?.groupBy?.expressions?.map((g) => g.property?.name) || []; |
||||
const aggregateColumns = builderQuery?.reduce?.expressions?.map((r) => r.property?.name) || []; |
||||
const selectedColumns = builderQuery?.columns?.columns || []; |
||||
|
||||
const allAvailableColumns = |
||||
groupByColumns.length > 0 |
||||
? groupByColumns |
||||
: aggregateColumns.length > 0 |
||||
? aggregateColumns |
||||
: selectedColumns.length > 0 |
||||
? selectedColumns |
||||
: allColumns.map((col) => col.name); |
||||
|
||||
const columnOptions = allAvailableColumns.map((col) => ({ |
||||
label: col, |
||||
value: col, |
||||
})); |
||||
|
||||
const orderOptions: Array<SelectableValue<string>> = [ |
||||
{ label: 'Ascending', value: 'asc' }, |
||||
{ label: 'Descending', value: 'desc' }, |
||||
]; |
||||
|
||||
const handleOrderByChange = (index: number, key: 'column' | 'order', value: string) => { |
||||
setOrderBy((prev) => { |
||||
const updated = [...prev]; |
||||
|
||||
if (index === -1) { |
||||
updated.push({ |
||||
property: { name: value, type: BuilderQueryEditorPropertyType.String }, |
||||
order: BuilderQueryEditorOrderByOptions.Asc, |
||||
type: BuilderQueryEditorExpressionType.Order_by, |
||||
}); |
||||
} else { |
||||
updated[index] = { |
||||
...updated[index], |
||||
property: |
||||
key === 'column' ? { name: value, type: BuilderQueryEditorPropertyType.String } : updated[index].property, |
||||
order: |
||||
key === 'order' && |
||||
(value === BuilderQueryEditorOrderByOptions.Asc || value === BuilderQueryEditorOrderByOptions.Desc) |
||||
? value |
||||
: updated[index].order, |
||||
}; |
||||
} |
||||
|
||||
buildAndUpdateQuery({ |
||||
orderBy: updated, |
||||
}); |
||||
|
||||
return updated; |
||||
}); |
||||
}; |
||||
|
||||
const onDeleteOrderBy = (index: number) => { |
||||
setOrderBy((prev) => { |
||||
const updated = prev.filter((_, i) => i !== index); |
||||
|
||||
buildAndUpdateQuery({ |
||||
orderBy: updated, |
||||
}); |
||||
|
||||
return updated; |
||||
}); |
||||
}; |
||||
|
||||
return ( |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField |
||||
label="Order By" |
||||
optional={true} |
||||
tooltip={`Sort results based on one or more columns in ascending or descending order.`} |
||||
> |
||||
<> |
||||
{orderBy.length > 0 ? ( |
||||
orderBy.map((entry, index) => ( |
||||
<InputGroup key={index}> |
||||
<Select |
||||
aria-label="Order by column" |
||||
width={inputFieldSize} |
||||
value={entry.property?.name ? { label: entry.property.name, value: entry.property.name } : null} |
||||
options={columnOptions} |
||||
onChange={(e) => e.value && handleOrderByChange(index, 'column', e.value)} |
||||
/> |
||||
<Label style={{ margin: '9px 9px 0 9px' }}>BY</Label> |
||||
<Select |
||||
aria-label="Order Direction" |
||||
width={inputFieldSize} |
||||
value={orderOptions.find((o) => o.value === entry.order) || null} |
||||
options={orderOptions} |
||||
onChange={(e) => e.value && handleOrderByChange(index, 'order', e.value)} |
||||
/> |
||||
<Button variant="secondary" icon="times" onClick={() => onDeleteOrderBy(index)} /> |
||||
{index === orderBy.length - 1 ? ( |
||||
<Button |
||||
variant="secondary" |
||||
onClick={() => handleOrderByChange(-1, 'column', '')} |
||||
icon="plus" |
||||
style={{ marginLeft: '15px' }} |
||||
/> |
||||
) : ( |
||||
<></> |
||||
)} |
||||
</InputGroup> |
||||
)) |
||||
) : ( |
||||
<InputGroup> |
||||
<Button variant="secondary" onClick={() => handleOrderByChange(-1, 'column', '')} icon="plus" /> |
||||
</InputGroup> |
||||
)} |
||||
</> |
||||
</EditorField> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
); |
||||
}; |
@ -0,0 +1,163 @@ |
||||
import React from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { EditorField, EditorFieldGroup, EditorRow, InputGroup } from '@grafana/plugin-ui'; |
||||
import { Button, Select } from '@grafana/ui'; |
||||
|
||||
import { BuilderQueryEditorExpressionType, BuilderQueryEditorPropertyType } from '../../dataquery.gen'; |
||||
import { AzureMonitorQuery, AzureLogAnalyticsMetadataColumn, AzureLogAnalyticsMetadataTable } from '../../types'; |
||||
|
||||
import { BuildAndUpdateOptions, inputFieldSize } from './utils'; |
||||
|
||||
interface TableSectionProps { |
||||
allColumns: AzureLogAnalyticsMetadataColumn[]; |
||||
tables: AzureLogAnalyticsMetadataTable[]; |
||||
query: AzureMonitorQuery; |
||||
buildAndUpdateQuery: (options: Partial<BuildAndUpdateOptions>) => void; |
||||
templateVariableOptions?: SelectableValue<string>; |
||||
} |
||||
|
||||
export const TableSection: React.FC<TableSectionProps> = (props) => { |
||||
const { allColumns, query, tables, buildAndUpdateQuery, templateVariableOptions } = props; |
||||
const builderQuery = query.azureLogAnalytics?.builderQuery; |
||||
const selectedColumns = query.azureLogAnalytics?.builderQuery?.columns?.columns || []; |
||||
|
||||
const tableOptions: Array<SelectableValue<string>> = tables.map((t) => ({ |
||||
label: t.name, |
||||
value: t.name, |
||||
})); |
||||
|
||||
const columnOptions: Array<SelectableValue<string>> = allColumns.map((col) => ({ |
||||
label: col.name, |
||||
value: col.name, |
||||
type: col.type, |
||||
})); |
||||
|
||||
const selectAllOption: SelectableValue<string> = { |
||||
label: 'All Columns', |
||||
value: '__all_columns__', |
||||
}; |
||||
|
||||
const selectableOptions: Array<SelectableValue<string>> = [ |
||||
selectAllOption, |
||||
...columnOptions, |
||||
...(templateVariableOptions |
||||
? Array.isArray(templateVariableOptions) |
||||
? templateVariableOptions |
||||
: [templateVariableOptions] |
||||
: []), |
||||
]; |
||||
|
||||
const handleTableChange = (selected: SelectableValue<string>) => { |
||||
const selectedTable = tables.find((t) => t.name === selected.value); |
||||
if (!selectedTable) { |
||||
return; |
||||
} |
||||
|
||||
buildAndUpdateQuery({ |
||||
from: { |
||||
property: { |
||||
name: selectedTable.name, |
||||
type: BuilderQueryEditorPropertyType.String, |
||||
}, |
||||
type: BuilderQueryEditorExpressionType.Property, |
||||
}, |
||||
reduce: [], |
||||
where: [], |
||||
fuzzySearch: [], |
||||
groupBy: [], |
||||
orderBy: [], |
||||
columns: [], |
||||
}); |
||||
}; |
||||
|
||||
const handleColumnsChange = (selected: SelectableValue<string> | Array<SelectableValue<string>> | null) => { |
||||
const selectedArray = Array.isArray(selected) ? selected : selected ? [selected] : []; |
||||
|
||||
if (selectedArray.length === 0) { |
||||
buildAndUpdateQuery({ columns: [] }); |
||||
return; |
||||
} |
||||
|
||||
const includesAll = selectedArray.some((opt) => opt.value === '__all_columns__'); |
||||
const lastSelected = selectedArray[selectedArray.length - 1]; |
||||
|
||||
if (includesAll && lastSelected.value === '__all_columns__') { |
||||
buildAndUpdateQuery({ |
||||
columns: allColumns.map((col) => col.name), |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
if (includesAll && selectedArray.length > 1) { |
||||
const filtered = selectedArray.filter((opt) => opt.value !== '__all_columns__'); |
||||
buildAndUpdateQuery({ |
||||
columns: filtered.map((opt) => opt.value!), |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
if (includesAll && selectedArray.length === 1) { |
||||
buildAndUpdateQuery({ |
||||
columns: allColumns.map((col) => col.name), |
||||
}); |
||||
return; |
||||
} |
||||
|
||||
buildAndUpdateQuery({ |
||||
columns: selectedArray.map((opt) => opt.value!), |
||||
}); |
||||
}; |
||||
|
||||
const onDeleteAllColumns = () => { |
||||
buildAndUpdateQuery({ |
||||
columns: [], |
||||
}); |
||||
}; |
||||
|
||||
const allColumnNames = allColumns.length > 0 ? allColumns.map((col) => col.name) : []; |
||||
|
||||
const areAllColumnsSelected = |
||||
allColumnNames.length > 0 && |
||||
selectedColumns.length > 0 && |
||||
selectedColumns.length === allColumnNames.length && |
||||
allColumnNames.every((col) => selectedColumns.includes(col)); |
||||
|
||||
const columnSelectValue: Array<SelectableValue<string>> = areAllColumnsSelected |
||||
? [selectAllOption] |
||||
: selectedColumns.map((col) => ({ label: col, value: col })); |
||||
|
||||
return ( |
||||
<EditorRow> |
||||
<EditorFieldGroup> |
||||
<EditorField label="Table"> |
||||
<Select |
||||
aria-label="Table" |
||||
value={builderQuery?.from?.property.name} |
||||
options={tableOptions} |
||||
placeholder="Select a table" |
||||
onChange={handleTableChange} |
||||
width={inputFieldSize} |
||||
/> |
||||
</EditorField> |
||||
<EditorField label="Columns"> |
||||
<InputGroup> |
||||
<Select |
||||
aria-label="Columns" |
||||
isMulti |
||||
isClearable |
||||
closeMenuOnSelect={false} |
||||
value={columnSelectValue} |
||||
options={selectableOptions} |
||||
placeholder="Select columns" |
||||
onChange={handleColumnsChange} |
||||
isDisabled={!builderQuery?.from?.property.name} |
||||
width={30} |
||||
/> |
||||
<Button variant="secondary" icon="times" onClick={onDeleteAllColumns} /> |
||||
</InputGroup> |
||||
</EditorField> |
||||
</EditorFieldGroup> |
||||
</EditorRow> |
||||
); |
||||
}; |
@ -0,0 +1,49 @@ |
||||
import { QueryEditorProperty, QueryEditorPropertyType } from '../../types'; |
||||
|
||||
export enum QueryEditorExpressionType { |
||||
Property = 'property', |
||||
Operator = 'operator', |
||||
Reduce = 'reduce', |
||||
FunctionParameter = 'functionParameter', |
||||
GroupBy = 'groupBy', |
||||
Or = 'or', |
||||
And = 'and', |
||||
} |
||||
|
||||
export interface QueryEditorExpression { |
||||
type: QueryEditorExpressionType; |
||||
} |
||||
|
||||
export interface QueryEditorFunctionParameterExpression extends QueryEditorExpression { |
||||
value: string; |
||||
fieldType: QueryEditorPropertyType; |
||||
name: string; |
||||
} |
||||
|
||||
export interface QueryEditorReduceExpression extends QueryEditorExpression { |
||||
property: QueryEditorProperty; |
||||
reduce: QueryEditorProperty; |
||||
parameters?: QueryEditorFunctionParameterExpression[]; |
||||
focus?: boolean; |
||||
} |
||||
|
||||
export interface QueryEditorGroupByExpression extends QueryEditorExpression { |
||||
property: QueryEditorProperty; |
||||
interval?: QueryEditorProperty; |
||||
focus?: boolean; |
||||
} |
||||
|
||||
export interface QueryEditorArrayExpression extends QueryEditorExpression { |
||||
expressions: QueryEditorExpression[] | QueryEditorArrayExpression[]; |
||||
} |
||||
|
||||
export interface QueryEditorReduceExpression extends QueryEditorExpression { |
||||
property: QueryEditorProperty; |
||||
reduce: QueryEditorProperty; |
||||
parameters?: QueryEditorFunctionParameterExpression[]; |
||||
focus?: boolean; |
||||
} |
||||
|
||||
export interface QueryEditorPropertyExpression extends QueryEditorExpression { |
||||
property: QueryEditorProperty; |
||||
} |
@ -0,0 +1,102 @@ |
||||
import { escapeRegExp } from 'lodash'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
|
||||
import { |
||||
BuilderQueryExpression, |
||||
BuilderQueryEditorExpressionType, |
||||
BuilderQueryEditorPropertyType, |
||||
BuilderQueryEditorReduceExpression, |
||||
BuilderQueryEditorWhereExpression, |
||||
BuilderQueryEditorGroupByExpression, |
||||
BuilderQueryEditorOrderByExpression, |
||||
BuilderQueryEditorPropertyExpression, |
||||
} from '../../dataquery.gen'; |
||||
import { AzureLogAnalyticsMetadataColumn, AzureMonitorQuery } from '../../types'; |
||||
|
||||
const DYNAMIC_TYPE_ARRAY_DELIMITER = '["`indexer`"]'; |
||||
export const inputFieldSize = 20; |
||||
|
||||
export const valueToDefinition = (name: string) => { |
||||
return { |
||||
value: name, |
||||
label: name.replace(new RegExp(escapeRegExp(DYNAMIC_TYPE_ARRAY_DELIMITER), 'g'), '[ ]'), |
||||
}; |
||||
}; |
||||
|
||||
export const DEFAULT_LOGS_BUILDER_QUERY: BuilderQueryExpression = { |
||||
columns: { columns: [], type: BuilderQueryEditorExpressionType.Property }, |
||||
from: { |
||||
type: BuilderQueryEditorExpressionType.Property, |
||||
property: { type: BuilderQueryEditorPropertyType.String, name: '' }, |
||||
}, |
||||
groupBy: { expressions: [], type: BuilderQueryEditorExpressionType.Group_by }, |
||||
reduce: { expressions: [], type: BuilderQueryEditorExpressionType.Reduce }, |
||||
where: { expressions: [], type: BuilderQueryEditorExpressionType.And }, |
||||
limit: 1000, |
||||
}; |
||||
|
||||
export const OPERATORS_BY_TYPE: Record<string, Array<SelectableValue<string>>> = { |
||||
string: [ |
||||
{ label: '==', value: '==' }, |
||||
{ label: '!=', value: '!=' }, |
||||
{ label: 'contains', value: 'contains' }, |
||||
{ label: '!contains', value: '!contains' }, |
||||
{ label: 'startswith', value: 'startswith' }, |
||||
{ label: 'endswith', value: 'endswith' }, |
||||
], |
||||
int: [ |
||||
{ label: '==', value: '==' }, |
||||
{ label: '!=', value: '!=' }, |
||||
{ label: '>', value: '>' }, |
||||
{ label: '<', value: '<' }, |
||||
{ label: '>=', value: '>=' }, |
||||
{ label: '<=', value: '<=' }, |
||||
], |
||||
datetime: [ |
||||
{ label: 'before', value: '<' }, |
||||
{ label: 'after', value: '>' }, |
||||
{ label: 'between', value: 'between' }, |
||||
], |
||||
bool: [ |
||||
{ label: '==', value: '==' }, |
||||
{ label: '!=', value: '!=' }, |
||||
], |
||||
}; |
||||
|
||||
export const toOperatorOptions = (type: string): Array<SelectableValue<string>> => { |
||||
return OPERATORS_BY_TYPE[type] || OPERATORS_BY_TYPE.string; |
||||
}; |
||||
|
||||
export const removeExtraQuotes = (value: string): string => { |
||||
let strValue = String(value).trim(); |
||||
if ((strValue.startsWith("'") && strValue.endsWith("'")) || (strValue.startsWith('"') && strValue.endsWith('"'))) { |
||||
return strValue.slice(1, -1); |
||||
} |
||||
return strValue; |
||||
}; |
||||
|
||||
export interface BuildAndUpdateOptions { |
||||
query: AzureMonitorQuery; |
||||
onQueryUpdate: (newQuery: AzureMonitorQuery) => void; |
||||
allColumns: AzureLogAnalyticsMetadataColumn[]; |
||||
limit?: number; |
||||
reduce?: BuilderQueryEditorReduceExpression[]; |
||||
where?: BuilderQueryEditorWhereExpression[]; |
||||
fuzzySearch?: BuilderQueryEditorWhereExpression[]; |
||||
groupBy?: BuilderQueryEditorGroupByExpression[]; |
||||
orderBy?: BuilderQueryEditorOrderByExpression[]; |
||||
columns?: string[]; |
||||
from?: BuilderQueryEditorPropertyExpression; |
||||
} |
||||
|
||||
export const aggregateOptions = [ |
||||
{ label: 'sum', value: 'sum' }, |
||||
{ label: 'avg', value: 'avg' }, |
||||
{ label: 'percentile', value: 'percentile' }, |
||||
{ label: 'count', value: 'count' }, |
||||
{ label: 'min', value: 'min' }, |
||||
{ label: 'max', value: 'max' }, |
||||
{ label: 'dcount', value: 'dcount' }, |
||||
{ label: 'stdev', value: 'stdev' }, |
||||
]; |
Loading…
Reference in new issue