mirror of https://github.com/grafana/grafana
AzureMonitor: Improve Log Analytics query efficiency (#74675)
* Promisify loading schema - Move schema loading to LogsQueryEditor - Improve typing - Switch callbacks to promises * Update types * Refactor backend for new props - Rename intersectTime - Support setting timeColumn - Add additional properties to logs request body * Update applyTemplateVariables * Update set functions * Add new TimeManagement component * Update LogsQueryEditor * Hardcode timestamp column for traces queries * Ensure timeColumn is always set for log queries * Update tests * Update frontend tests * Readd type to make migration easier * Add migration * Add fake schema * Use predefined type * Update checks and defaults * Add tests * README updates * README update * Type update * Lint * More linting and type fixing * Error silently * More linting and typing * Update betterer * Update test * Simplify default column setting * Fix default column setting * Add tracking * Review - Fix typo on comment - Destructure and remove type assertion - Break out await into two variables - Remove lets and rename variable for claritypull/75028/head
parent
025979df75
commit
b779ce5687
@ -0,0 +1,172 @@ |
||||
import { render, screen } from '@testing-library/react'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import React from 'react'; |
||||
|
||||
import createMockDatasource from '../../__mocks__/datasource'; |
||||
import createMockQuery from '../../__mocks__/query'; |
||||
import FakeSchemaData from '../../azure_log_analytics/__mocks__/schema'; |
||||
|
||||
import { TimeManagement } from './TimeManagement'; |
||||
|
||||
const variableOptionGroup = { |
||||
label: 'Template variables', |
||||
options: [], |
||||
}; |
||||
|
||||
describe('LogsQueryEditor.TimeManagement', () => { |
||||
it('should render the column picker if Dashboard is chosen', async () => { |
||||
const mockDatasource = createMockDatasource(); |
||||
const query = createMockQuery({ azureLogAnalytics: { timeColumn: undefined } }); |
||||
const onChange = jest.fn(); |
||||
|
||||
const { rerender } = render( |
||||
<TimeManagement |
||||
query={query} |
||||
datasource={mockDatasource} |
||||
variableOptionGroup={variableOptionGroup} |
||||
onQueryChange={onChange} |
||||
setError={() => {}} |
||||
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema()} |
||||
/> |
||||
); |
||||
|
||||
const dashboardTimeOption = await screen.findByLabelText('Dashboard'); |
||||
await userEvent.click(dashboardTimeOption); |
||||
|
||||
expect(onChange).toBeCalledWith( |
||||
expect.objectContaining({ |
||||
azureLogAnalytics: expect.objectContaining({ |
||||
dashboardTime: true, |
||||
}), |
||||
}) |
||||
); |
||||
|
||||
rerender( |
||||
<TimeManagement |
||||
query={{ ...query, azureLogAnalytics: { ...query.azureLogAnalytics, dashboardTime: true } }} |
||||
datasource={mockDatasource} |
||||
variableOptionGroup={variableOptionGroup} |
||||
onQueryChange={onChange} |
||||
setError={() => {}} |
||||
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema()} |
||||
/> |
||||
); |
||||
|
||||
expect(onChange).toBeCalledWith( |
||||
expect.objectContaining({ |
||||
azureLogAnalytics: expect.objectContaining({ |
||||
timeColumn: 'TimeGenerated', |
||||
}), |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('should render the default value if no time columns exist', async () => { |
||||
const mockDatasource = createMockDatasource(); |
||||
const query = createMockQuery(); |
||||
const onChange = jest.fn(); |
||||
|
||||
render( |
||||
<TimeManagement |
||||
query={{ |
||||
...query, |
||||
azureLogAnalytics: { ...query.azureLogAnalytics, dashboardTime: true, timeColumn: undefined }, |
||||
}} |
||||
datasource={mockDatasource} |
||||
variableOptionGroup={variableOptionGroup} |
||||
onQueryChange={onChange} |
||||
setError={() => {}} |
||||
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([ |
||||
{ |
||||
id: 't/Alert', |
||||
name: 'Alert', |
||||
timespanColumn: 'TimeGenerated', |
||||
columns: [], |
||||
related: { |
||||
solutions: [], |
||||
}, |
||||
}, |
||||
])} |
||||
/> |
||||
); |
||||
|
||||
expect(onChange).toBeCalledWith( |
||||
expect.objectContaining({ |
||||
azureLogAnalytics: expect.objectContaining({ |
||||
timeColumn: 'TimeGenerated', |
||||
}), |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('should render the first time column if no default exists', async () => { |
||||
const mockDatasource = createMockDatasource(); |
||||
const query = createMockQuery(); |
||||
const onChange = jest.fn(); |
||||
|
||||
render( |
||||
<TimeManagement |
||||
query={{ |
||||
...query, |
||||
azureLogAnalytics: { ...query.azureLogAnalytics, dashboardTime: true, timeColumn: undefined }, |
||||
}} |
||||
datasource={mockDatasource} |
||||
variableOptionGroup={variableOptionGroup} |
||||
onQueryChange={onChange} |
||||
setError={() => {}} |
||||
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([ |
||||
{ |
||||
id: 't/Alert', |
||||
name: 'Alert', |
||||
timespanColumn: '', |
||||
columns: [{ name: 'Timespan', type: 'datetime' }], |
||||
related: { |
||||
solutions: [], |
||||
}, |
||||
}, |
||||
])} |
||||
/> |
||||
); |
||||
|
||||
expect(onChange).toBeCalledWith( |
||||
expect.objectContaining({ |
||||
azureLogAnalytics: expect.objectContaining({ |
||||
timeColumn: 'Timespan', |
||||
}), |
||||
}) |
||||
); |
||||
}); |
||||
|
||||
it('should render the query time column if it exists', async () => { |
||||
const mockDatasource = createMockDatasource(); |
||||
const query = createMockQuery(); |
||||
const onChange = jest.fn(); |
||||
|
||||
render( |
||||
<TimeManagement |
||||
query={{ |
||||
...query, |
||||
azureLogAnalytics: { ...query.azureLogAnalytics, dashboardTime: true, timeColumn: 'TestTimeColumn' }, |
||||
}} |
||||
datasource={mockDatasource} |
||||
variableOptionGroup={variableOptionGroup} |
||||
onQueryChange={onChange} |
||||
setError={() => {}} |
||||
schema={FakeSchemaData.getLogAnalyticsFakeEngineSchema([ |
||||
{ |
||||
id: 't/Alert', |
||||
name: 'Alert', |
||||
timespanColumn: '', |
||||
columns: [{ name: 'TestTimeColumn', type: 'datetime' }], |
||||
related: { |
||||
solutions: [], |
||||
}, |
||||
}, |
||||
])} |
||||
/> |
||||
); |
||||
|
||||
expect(onChange).not.toBeCalled(); |
||||
expect(screen.getByText('Alert > TestTimeColumn')).toBeInTheDocument(); |
||||
}); |
||||
}); |
||||
@ -0,0 +1,137 @@ |
||||
import React, { useCallback, useEffect, useState } from 'react'; |
||||
|
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { InlineField, RadioButtonGroup, Select } from '@grafana/ui'; |
||||
|
||||
import { AzureQueryEditorFieldProps } from '../../types'; |
||||
|
||||
import { setDashboardTime, setTimeColumn } from './setQueryValue'; |
||||
|
||||
export function TimeManagement({ query, onQueryChange: onChange, schema }: AzureQueryEditorFieldProps) { |
||||
const [defaultTimeColumns, setDefaultTimeColumns] = useState<SelectableValue[] | undefined>(); |
||||
const [timeColumns, setTimeColumns] = useState<SelectableValue[] | undefined>(); |
||||
|
||||
const setDefaultColumn = useCallback((column: string) => onChange(setTimeColumn(query, column)), [query, onChange]); |
||||
|
||||
useEffect(() => { |
||||
if (schema && query.azureLogAnalytics?.dashboardTime) { |
||||
const timeColumnOptions: SelectableValue[] = []; |
||||
const timeColumnsSet: Set<string> = new Set(); |
||||
const defaultColumnsMap: Map<string, SelectableValue> = new Map(); |
||||
const db = schema.database; |
||||
if (db) { |
||||
for (const table of db.tables) { |
||||
const cols = table.columns.reduce<SelectableValue[]>((prev, curr, i) => { |
||||
if (curr.type === 'datetime') { |
||||
if (!table.timespanColumn || table.timespanColumn !== curr.name) { |
||||
prev.push({ value: curr.name, label: `${table.name} > ${curr.name}` }); |
||||
timeColumnsSet.add(curr.name); |
||||
} |
||||
} |
||||
return prev; |
||||
}, []); |
||||
timeColumnOptions.push(...cols); |
||||
if (table.timespanColumn && !defaultColumnsMap.has(table.timespanColumn)) { |
||||
defaultColumnsMap.set(table.timespanColumn, { |
||||
value: table.timespanColumn, |
||||
label: table.timespanColumn, |
||||
}); |
||||
} |
||||
} |
||||
} |
||||
setTimeColumns(timeColumnOptions); |
||||
const defaultColumns = Array.from(defaultColumnsMap.values()); |
||||
setDefaultTimeColumns(defaultColumns); |
||||
|
||||
// Set default value
|
||||
if ( |
||||
!query.azureLogAnalytics.timeColumn || |
||||
(query.azureLogAnalytics.timeColumn && |
||||
!timeColumnsSet.has(query.azureLogAnalytics.timeColumn) && |
||||
!defaultColumnsMap.has(query.azureLogAnalytics.timeColumn)) |
||||
) { |
||||
if (defaultColumns && defaultColumns.length) { |
||||
setDefaultColumn(defaultColumns[0].value); |
||||
setDefaultColumn(defaultColumns[0].value); |
||||
return; |
||||
} else if (timeColumnOptions && timeColumnOptions.length) { |
||||
setDefaultColumn(timeColumnOptions[0].value); |
||||
return; |
||||
} else { |
||||
setDefaultColumn('TimeGenerated'); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
}, [schema, query.azureLogAnalytics?.dashboardTime, query.azureLogAnalytics?.timeColumn, setDefaultColumn]); |
||||
|
||||
const handleTimeColumnChange = useCallback( |
||||
(change: SelectableValue<string>) => { |
||||
if (!change.value) { |
||||
return; |
||||
} |
||||
|
||||
const newQuery = setTimeColumn(query, change.value); |
||||
onChange(newQuery); |
||||
}, |
||||
[onChange, query] |
||||
); |
||||
return ( |
||||
<> |
||||
<InlineField |
||||
label="Time-range" |
||||
tooltip={ |
||||
<span> |
||||
Specifies the time-range used to query. The <code>Query</code> option will only use time-ranges specified in |
||||
the query. <code>Dashboard</code> will only use the Grafana time-range. |
||||
</span> |
||||
} |
||||
> |
||||
<RadioButtonGroup |
||||
options={[ |
||||
{ label: 'Query', value: false }, |
||||
{ label: 'Dashboard', value: true }, |
||||
]} |
||||
value={query.azureLogAnalytics?.dashboardTime ?? false} |
||||
size={'md'} |
||||
onChange={(val) => onChange(setDashboardTime(query, val))} |
||||
/> |
||||
</InlineField> |
||||
{query.azureLogAnalytics?.dashboardTime && ( |
||||
<InlineField |
||||
label="Time Column" |
||||
tooltip={ |
||||
<span> |
||||
Specifies the time column used for filtering. Defaults to the first tables <code>timeSpan</code> column, |
||||
the first <code>datetime</code> column found or <code>TimeGenerated</code>. |
||||
</span> |
||||
} |
||||
> |
||||
<Select |
||||
options={[ |
||||
{ |
||||
label: 'Default time columns', |
||||
options: defaultTimeColumns ?? [{ value: 'TimeGenerated', label: 'TimeGenerated' }], |
||||
}, |
||||
{ |
||||
label: 'Other time columns', |
||||
options: timeColumns ?? [], |
||||
}, |
||||
]} |
||||
onChange={handleTimeColumnChange} |
||||
value={ |
||||
query.azureLogAnalytics?.timeColumn |
||||
? query.azureLogAnalytics?.timeColumn |
||||
: defaultTimeColumns |
||||
? defaultTimeColumns[0] |
||||
: timeColumns |
||||
? timeColumns[0] |
||||
: { value: 'TimeGenerated', label: 'TimeGenerated' } |
||||
} |
||||
allowCustomValue |
||||
/> |
||||
</InlineField> |
||||
)} |
||||
</> |
||||
); |
||||
} |
||||
Loading…
Reference in new issue