From 80486be6ac4a8212e65b6f18a70036393480774c Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 6 Oct 2021 11:46:12 +0200 Subject: [PATCH 1/4] refactor query editor code --- src/QueryCodeEditor.test.tsx | 71 +++++++++++++ src/QueryCodeEditor.tsx | 89 ++++++++++++++++ src/QueryEditor.test.tsx | 13 +-- src/QueryEditor.tsx | 198 +++++++++-------------------------- src/ResourceMacro.test.tsx | 44 ++++---- src/ResourceMacro.tsx | 23 ++-- src/SchemaInfo.test.ts | 168 ----------------------------- src/SchemaInfo.ts | 177 ------------------------------- src/Suggestions.test.ts | 35 +++++++ src/Suggestions.ts | 73 +++++++++++++ src/__mocks__/datasource.ts | 3 - src/datasource.ts | 13 +-- src/variables.ts | 22 ++-- 13 files changed, 372 insertions(+), 557 deletions(-) create mode 100644 src/QueryCodeEditor.test.tsx create mode 100644 src/QueryCodeEditor.tsx delete mode 100644 src/SchemaInfo.test.ts delete mode 100644 src/SchemaInfo.ts create mode 100644 src/Suggestions.test.ts create mode 100644 src/Suggestions.ts diff --git a/src/QueryCodeEditor.test.tsx b/src/QueryCodeEditor.test.tsx new file mode 100644 index 00000000..993f1196 --- /dev/null +++ b/src/QueryCodeEditor.test.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { QueryCodeEditor } from './QueryCodeEditor'; +import { mockDatasource, mockQuery } from './__mocks__/datasource'; +import '@testing-library/jest-dom'; +import { select } from 'react-select-event'; + +const ds = mockDatasource; +const q = mockQuery; + +const props = { + datasource: ds, + query: q, + onChange: jest.fn(), + onRunQuery: jest.fn(), +}; + +beforeEach(() => { + ds.getResource = jest.fn().mockResolvedValue([]); + ds.postResource = jest.fn().mockResolvedValue([]); +}); + +describe('QueryCodeEditor', () => { + it('should list and select an schema', async () => { + ds.getResource = jest.fn().mockResolvedValue(['public', 'foo']); + const onChange = jest.fn(); + render(); + const selectEl = screen.getByText('$__schema = public'); + expect(selectEl).toBeInTheDocument(); + selectEl.click(); + + await select(selectEl, 'foo', { container: document.body }); + + expect(onChange).toHaveBeenCalledWith({ + ...q, + schema: 'foo', + }); + }); + + it('should list and select a table', async () => { + ds.postResource = jest.fn().mockResolvedValue(['foo', 'bar']); + const onChange = jest.fn(); + render(); + const selectEl = screen.getByText('$__table = ?'); + expect(selectEl).toBeInTheDocument(); + selectEl.click(); + + await select(selectEl, 'foo', { container: document.body }); + + expect(onChange).toHaveBeenCalledWith({ + ...q, + table: 'foo', + }); + }); + + it('should list and select a column', async () => { + ds.postResource = jest.fn().mockResolvedValue(['foo', 'bar']); + const onChange = jest.fn(); + render(); + const selectEl = screen.getByText('$__column = ?'); + expect(selectEl).toBeInTheDocument(); + selectEl.click(); + + await select(selectEl, 'foo', { container: document.body }); + + expect(onChange).toHaveBeenCalledWith({ + ...q, + column: 'foo', + }); + }); +}); diff --git a/src/QueryCodeEditor.tsx b/src/QueryCodeEditor.tsx new file mode 100644 index 00000000..8e2afe81 --- /dev/null +++ b/src/QueryCodeEditor.tsx @@ -0,0 +1,89 @@ +import { defaults } from 'lodash'; + +import React from 'react'; +import { QueryEditorProps } from '@grafana/data'; +import { DataSource } from './datasource'; +import { defaultQuery, RedshiftDataSourceOptions, RedshiftQuery } from './types'; +import { CodeEditor, InlineFormLabel } from '@grafana/ui'; +import { getTemplateSrv } from '@grafana/runtime'; +import ResourceMacro from 'ResourceMacro'; +import { getSuggestions } from 'Suggestions'; + +type Props = QueryEditorProps; + +export function QueryCodeEditor(props: Props) { + const onChange = (value: RedshiftQuery) => { + props.onChange(value); + props.onRunQuery(); + }; + + const onRawSqlChange = (rawSQL: string) => { + props.onChange({ + ...props.query, + rawSQL, + }); + props.onRunQuery(); + }; + + const { rawSQL } = defaults(props.query, defaultQuery); + + const loadSchemas = async () => { + const schemas: string[] = await props.datasource.getResource('schemas'); + return schemas.map((schema) => ({ label: schema, value: schema })).concat({ label: '-- remove --', value: '' }); + }; + + const loadTables = async () => { + const tables: string[] = await props.datasource.postResource('tables', { + schema: props.query.schema || '', + }); + return tables.map((table) => ({ label: table, value: table })).concat({ label: '-- remove --', value: '' }); + }; + + const loadColumns = async () => { + const columns: string[] = await props.datasource.postResource('columns', { + table: props.query.table, + }); + return columns.map((column) => ({ label: column, value: column })).concat({ label: '-- remove --', value: '' }); + }; + + return ( + <> +
+ + Macros + + {ResourceMacro({ + resource: 'schema', + query: props.query, + loadOptions: loadSchemas, + updateQuery: onChange, + })} + {ResourceMacro({ + resource: 'table', + query: props.query, + loadOptions: loadTables, + updateQuery: onChange, + })} + {ResourceMacro({ + resource: 'column', + query: props.query, + loadOptions: loadColumns, + updateQuery: onChange, + })} +
+
+
+
+ getSuggestions({ query: props.query, templateSrv: getTemplateSrv() })} + /> + + ); +} diff --git a/src/QueryEditor.test.tsx b/src/QueryEditor.test.tsx index 7c9d746b..71d4a50f 100644 --- a/src/QueryEditor.test.tsx +++ b/src/QueryEditor.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { QueryEditor } from './QueryEditor'; import { mockDatasource, mockQuery } from './__mocks__/datasource'; import '@testing-library/jest-dom'; @@ -21,19 +21,14 @@ const props = { describe('QueryEditor', () => { it('should render Macros input', async () => { render(); - expect(screen.getByText('$__schema = public')).toBeInTheDocument(); + await waitFor(() => screen.getByText('$__schema = public')); expect(screen.getByText('$__table = ?')).toBeInTheDocument(); expect(screen.getByText('$__column = ?')).toBeInTheDocument(); }); - it('should not include the Format As input if the query editor does not support multiple queries', async () => { - render(); - expect(screen.queryByText('Format as')).not.toBeInTheDocument(); - }); - it('should include the Format As input', async () => { - render(); - expect(screen.queryByText('Format as')).toBeInTheDocument(); + render(); + await waitFor(() => screen.getByText('Format as')); }); it('should allow to change the fill mode', async () => { diff --git a/src/QueryEditor.tsx b/src/QueryEditor.tsx index 8b00eb10..0df844e1 100644 --- a/src/QueryEditor.tsx +++ b/src/QueryEditor.tsx @@ -1,6 +1,6 @@ import { defaults } from 'lodash'; -import React, { PureComponent } from 'react'; +import React, { useState } from 'react'; import { QueryEditorProps } from '@grafana/data'; import { DataSource } from './datasource'; import { @@ -12,160 +12,66 @@ import { SelectableFillValueOptions, FillValueOptions, } from './types'; -import { CodeEditor, Alert, InlineField, Select, InlineFormLabel, Input, InlineFieldRow } from '@grafana/ui'; -import { SchemaInfo } from 'SchemaInfo'; -import { getTemplateSrv } from '@grafana/runtime'; -import ResourceMacro from 'ResourceMacro'; +import { InlineField, Select, Input, InlineFieldRow } from '@grafana/ui'; +import { QueryCodeEditor } from 'QueryCodeEditor'; type Props = QueryEditorProps; -interface State { - schema: SchemaInfo; - schemaState?: Partial; - fillValue: number; -} - -export class QueryEditor extends PureComponent { - state: State = { - schema: new SchemaInfo(this.props.datasource, this.props.query, getTemplateSrv()), - fillValue: 0, - }; +export function QueryEditor(props: Props) { + const [fillValue, setFillValue] = useState(0); - componentDidMount = () => { - const { schema } = this.state; - this.setState({ schemaState: schema.state }); - schema.preload(); + const onChange = (value: RedshiftQuery) => { + props.onChange(value); + props.onRunQuery(); }; - onChange = (value: RedshiftQuery) => { - this.props.onChange(value); - this.props.onRunQuery(); + const onFillValueChange = ({ currentTarget }: React.FormEvent) => { + setFillValue(currentTarget.valueAsNumber); }; - onRawSqlChange = (rawSQL: string) => { - this.props.onChange({ - ...this.props.query, - rawSQL, - }); - this.props.onRunQuery(); - }; - - updateSchemaState = (query: RedshiftQuery) => { - const schemaState = this.state.schema.updateState(query); - this.setState({ schemaState }); - - this.props.onChange(query); - this.props.onRunQuery(); - }; + const { format, fillMode } = defaults(props.query, defaultQuery); - isPanelEditor = () => { - // If there can be more than one query, it's a panel editor - return !!this.props.queries; - }; - - onFillValueChange = ({ currentTarget }: React.FormEvent) => { - this.setState({ fillValue: currentTarget.valueAsNumber }); - }; - - render() { - const { rawSQL, format, fillMode } = defaults(this.props.query, defaultQuery); - - const { schema, schemaState } = this.state; - return ( - <> - - To save and re-run the query, press ctrl/cmd+S. - -
- - Macros - - {schema && schemaState && ( - <> - {ResourceMacro({ - resource: 'schema', - schema, - query: this.props.query, - value: schemaState.schema, - updateSchemaState: this.updateSchemaState, - })} - {ResourceMacro({ - resource: 'table', - schema, - query: this.props.query, - value: schemaState.table, - updateSchemaState: this.updateSchemaState, - })} - {ResourceMacro({ - resource: 'column', - schema, - query: this.props.query, - value: schemaState.column, - updateSchemaState: this.updateSchemaState, - })} - - )} -
-
-
-
- {schema && ( - + + + + onChange({ + ...props.query, + fillMode: { mode: value || FillValueOptions.Previous, value: fillValue }, + }) + } /> + + {fillMode.mode === FillValueOptions.Value && ( + + + onChange({ + ...props.query, + fillMode: { mode: FillValueOptions.Value, value: fillValue }, + }) + } + /> + )} - {this.isPanelEditor() && ( - <> - - - this.onChange({ - ...this.props.query, - fillMode: { mode: value || FillValueOptions.Previous, value: this.state.fillValue }, - }) - } - /> - - {fillMode.mode === FillValueOptions.Value && ( - - - this.onChange({ - ...this.props.query, - fillMode: { mode: FillValueOptions.Value, value: this.state.fillValue }, - }) - } - /> - - )} - - - )} - - ); - } + + + ); } diff --git a/src/ResourceMacro.test.tsx b/src/ResourceMacro.test.tsx index 01ac6847..01dd87bb 100644 --- a/src/ResourceMacro.test.tsx +++ b/src/ResourceMacro.test.tsx @@ -1,15 +1,14 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import React from 'react'; import ResourceMacro, { Resource } from 'ResourceMacro'; -import { SchemaInfo } from 'SchemaInfo'; -import { mockDatasource, mockQuery, mockSchemaInfo } from '__mocks__/datasource'; -import userEvent from '@testing-library/user-event'; +import { mockQuery } from '__mocks__/datasource'; +import { select } from 'react-select-event'; const defaultProps = { resource: 'table' as Resource, query: mockQuery, - schema: mockSchemaInfo, - updateSchemaState: jest.fn(), + updateQuery: jest.fn(), + loadOptions: jest.fn(), }; describe('ResourceMacro', () => { @@ -23,35 +22,34 @@ describe('ResourceMacro', () => { expect(screen.getByText('$__table = foo')).toBeInTheDocument(); }); - it('should load the resource options', () => { - const schema = new SchemaInfo(mockDatasource, mockQuery); - schema.getTables = jest.fn().mockReturnValue({ then: jest.fn() }); - render(); + it('should load the resource options', async () => { + const loadOptions = jest.fn().mockResolvedValue([]); + render(); const node = screen.getByText('$__table = ?'); - userEvent.click(node); - expect(schema.getTables).toHaveBeenCalled(); + node.click(); + expect(loadOptions).toHaveBeenCalled(); + await waitFor(() => screen.getByText('No options found')); }); it('should change the selected option', async () => { - const updateSchemaState = jest.fn(); - const schema = new SchemaInfo(mockDatasource, mockQuery); - schema.getTables = jest.fn().mockResolvedValue([ + const loadOptions = jest.fn().mockResolvedValue([ { label: 'foo', value: 'foo' }, { label: 'bar', value: 'bar' }, ]); + const updateQuery = jest.fn(); render( ); - // TODO: investigate why this throws a console.log error in our test suite - userEvent.click(screen.getByText('$__table = foo')); - expect(schema.getTables).toHaveBeenCalled(); - await screen.findByText('bar'); - userEvent.click(screen.getByText('bar')); - expect(updateSchemaState).toHaveBeenCalledWith({ ...mockQuery, table: 'bar' }); + const selectEl = screen.getByText('$__table = foo'); + expect(selectEl).toBeInTheDocument(); + selectEl.click(); + await select(selectEl, 'bar', { container: document.body }); + + expect(updateQuery).toHaveBeenCalledWith({ ...mockQuery, table: 'bar' }); }); }); diff --git a/src/ResourceMacro.tsx b/src/ResourceMacro.tsx index a3360c92..3199317d 100644 --- a/src/ResourceMacro.tsx +++ b/src/ResourceMacro.tsx @@ -1,7 +1,6 @@ import { SelectableValue } from '@grafana/data'; import { SegmentAsync } from '@grafana/ui'; import React from 'react'; -import { SchemaInfo } from 'SchemaInfo'; import { RedshiftQuery } from 'types'; export type Resource = 'schema' | 'table' | 'column'; @@ -9,17 +8,21 @@ export type Resource = 'schema' | 'table' | 'column'; interface Props { resource: Resource; query: RedshiftQuery; - schema: SchemaInfo; - updateSchemaState: (query: RedshiftQuery) => void; - value?: string; + updateQuery: (q: RedshiftQuery) => void; + loadOptions: (query?: string) => Promise>>; } -function ResourceMacro({ resource, schema, updateSchemaState, query, value }: Props) { +function ResourceMacro({ resource, query, updateQuery, loadOptions }: Props) { let placeholder = ''; let current = '$__' + resource + ' = '; if (query[resource]) { current += query[resource]; } else { + let value = query[resource]; + if (!value && resource === 'schema') { + // Use the public schema by default + value = 'public'; + } placeholder = current + (value ?? '?'); current = ''; } @@ -42,20 +45,14 @@ function ResourceMacro({ resource, schema, updateSchemaState, query, value }: Pr // Clean up column since a new table is set newQuery.column = undefined; } - updateSchemaState(newQuery); + updateQuery(newQuery); }; }; - const loadOptions = { - schema: schema.getSchemas, - table: schema.getTables, - column: schema.getColumns, - }; - return ( { - describe('constructor', () => { - it("should select the 'public' schema by default", () => { - const schema = new SchemaInfo(ds, q); - expect(schema.state.schema).toEqual('public'); - }); - }); - - describe('getSuggestions', () => { - const macros = [ - '$__timeEpoch', - '$__timeFilter', - '$__timeFrom', - '$__timeTo', - '$__timeGroup', - '$__unixEpochFilter', - '$__unixEpochGroup', - '$__schema', - '$__table', - '$__column', - ]; - it('should return the list of macros', () => { - const schema = new SchemaInfo(ds, q); - const sugs = schema.getSuggestions(); - expect(sugs.map((s) => s.label)).toEqual(macros); - }); - - it('should return the list of template variables', () => { - const templateSrv = { - getVariables: jest.fn().mockReturnValue([{ name: 'foo' }, { name: 'bar' }]), - replace: jest.fn(), - }; - const schema = new SchemaInfo(ds, q, templateSrv); - const sugs = schema.getSuggestions(); - expect(sugs.map((s) => s.label)).toEqual(macros.concat('$foo', '$bar')); - }); - }); - - describe('updateState', () => { - it('updates the schema in the state', () => { - const schema = new SchemaInfo(ds, q); - schema.updateState({ schema: 'foo' }); - expect(schema.state).toEqual({ ...q, schema: 'foo' }); - }); - - it('cleans up tables and columns if the schema changes', () => { - const schema = new SchemaInfo(ds, q); - schema.tables = [{ label: 'foo', value: 'foo' }]; - schema.columns = [{ label: 'bar', value: 'bar' }]; - schema.updateState({ schema: 'foobar' }); - expect(schema.tables).toBeUndefined(); - expect(schema.columns).toBeUndefined(); - }); - - it('sets a table in the state', () => { - const schema = new SchemaInfo(ds, q); - schema.updateState({ table: 'foo' }); - expect(schema.state.table).toEqual('foo'); - }); - - it('cleans up columns if the table changes', () => { - const schema = new SchemaInfo(ds, q); - schema.columns = [{ label: 'bar', value: 'bar' }]; - schema.updateState({ table: 'foobar' }); - expect(schema.columns).toBeUndefined(); - }); - - it('sets a column in the state', () => { - const schema = new SchemaInfo(ds, q); - schema.updateState({ column: 'foo' }); - expect(schema.state.column).toEqual('foo'); - }); - - it('uses the templateSrv to replace values', () => { - const templateSrv = { - getVariables: jest.fn(), - replace: (s: string) => s.replace('$', ''), - }; - const schema = new SchemaInfo(ds, q, templateSrv); - schema.updateState({ schema: '$foobar', table: '$foo', column: '$bar' }); - expect(schema.state).toEqual({ ...q, schema: 'foobar', table: 'foo', column: 'bar' }); - }); - }); - - describe('getSchemas', () => { - it('should return cached schemas', async () => { - const schema = new SchemaInfo(ds, q); - const schemas = [{ label: 'foo', value: 'foo' }]; - schema.schemas = schemas; - const res = await schema.getSchemas(); - expect(res).toEqual(schemas); - }); - - it('should get schemas as a resource', async () => { - ds.getResource = jest.fn().mockResolvedValue(['foo', 'bar']); - const schema = new SchemaInfo(ds, q); - const res = await schema.getSchemas(); - expect(res).toEqual([ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: '-- remove --', value: '' }, - ]); - }); - }); - - describe('getTables', () => { - it('should return cached tables', async () => { - const schema = new SchemaInfo(ds, q); - const tables = [{ label: 'foo', value: 'foo' }]; - schema.tables = tables; - const res = await schema.getTables(); - expect(res).toEqual(tables); - }); - - it('should get tables as a resource', async () => { - ds.postResource = jest.fn().mockResolvedValue(['foo', 'bar']); - const schema = new SchemaInfo(ds, q); - const res = await schema.getTables(); - expect(res).toEqual([ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: '-- remove --', value: '' }, - ]); - }); - - it('should get tables as a resource', async () => { - ds.postResource = jest.fn().mockResolvedValue(['foo', 'bar']); - const schema = new SchemaInfo(ds, q); - await schema.getTables(); - expect(ds.postResource).toHaveBeenCalledWith('tables', { schema: 'public' }); - }); - }); - - describe('getColumns', () => { - it('should return cached columns', async () => { - const schema = new SchemaInfo(ds, q); - const columns = [{ label: 'foo', value: 'foo' }]; - schema.columns = columns; - const res = await schema.getColumns(); - expect(res).toEqual(columns); - }); - - it('should get columns as a resource', async () => { - ds.postResource = jest.fn().mockResolvedValue(['foo', 'bar']); - const schema = new SchemaInfo(ds, q); - schema.state.table = 'foobar'; - const res = await schema.getColumns(); - expect(res).toEqual([ - { label: 'foo', value: 'foo' }, - { label: 'bar', value: 'bar' }, - { label: '-- remove --', value: '' }, - ]); - expect(ds.postResource).toHaveBeenCalledWith('columns', { table: 'foobar' }); - }); - - it('should return empty if the table is not set', async () => { - const schema = new SchemaInfo(ds, q); - const res = await schema.getColumns(); - expect(res).toEqual([{ label: 'table not configured', value: '' }]); - }); - }); -}); diff --git a/src/SchemaInfo.ts b/src/SchemaInfo.ts deleted file mode 100644 index de59db28..00000000 --- a/src/SchemaInfo.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { SelectableValue } from '@grafana/data'; -import { TemplateSrv } from '@grafana/runtime'; -import { CodeEditorSuggestionItem, CodeEditorSuggestionItemKind } from '@grafana/ui'; -import { RedshiftQuery } from 'types'; -import { DataSource } from './datasource'; - -export class SchemaInfo { - state: Partial; - - schemas?: Array>; - tables?: Array>; - columns?: Array>; - - constructor(private ds: DataSource, q: Partial, private templateSrv?: TemplateSrv) { - this.state = { ...q }; - if (!q.schema) { - // The default schema is "public" - this.state.schema = 'public'; - } - } - - updateState(state: Partial): Partial { - // Clean up related state - if (state.schema) { - this.tables = undefined; - this.columns = undefined; - } - if (state.table) { - this.columns = undefined; - } - - const merged = { ...this.state, ...state }; - if (this.templateSrv) { - if (merged.schema) { - merged.schema = this.templateSrv.replace(merged.schema); - } - if (merged.table) { - merged.table = this.templateSrv.replace(merged.table); - } - if (merged.column) { - merged.column = this.templateSrv.replace(merged.column); - } - } - return (this.state = merged); - } - - getSchemas = async (query?: string) => { - if (this.schemas) { - return Promise.resolve(this.schemas); - } - return this.ds.getResource('schemas').then((vals: string[]) => { - this.schemas = vals.map((name) => { - return { label: name, value: name }; - }); - this.schemas.push({ - label: '-- remove --', - value: '', - }); - return this.schemas; - }); - }; - - getTables = async (query?: string) => { - if (this.tables) { - return Promise.resolve(this.tables); - } - return this.ds.postResource('tables', { schema: this.state.schema || '' }).then((vals: string[]) => { - this.tables = vals.map((name) => { - return { label: name, value: name }; - }); - this.tables.push({ - label: '-- remove --', - value: '', - }); - return this.tables; - }); - }; - - getColumns = async (query?: string) => { - if (this.columns) { - return Promise.resolve(this.columns); - } - if (!this.state.table) { - return Promise.resolve([{ label: 'table not configured', value: '' }]); - } - return this.ds.postResource('columns', { table: this.state.table }).then((vals: string[]) => { - this.columns = vals.map((name) => { - return { label: name, value: name }; - }); - this.columns.push({ - label: '-- remove --', - value: '', - }); - return this.columns; - }); - }; - - async preload() { - await this.getSchemas(); - await this.getTables(); - if (this.state.table) { - this.getColumns(); - } - } - - getSuggestions = (): CodeEditorSuggestionItem[] => { - const sugs: CodeEditorSuggestionItem[] = [ - { - label: '$__timeEpoch', - kind: CodeEditorSuggestionItemKind.Method, - detail: '(Macro)', - }, - { - label: '$__timeFilter', - kind: CodeEditorSuggestionItemKind.Method, - detail: '(Macro)', - }, - { - label: '$__timeFrom', - kind: CodeEditorSuggestionItemKind.Method, - detail: '(Macro)', - }, - { - label: '$__timeTo', - kind: CodeEditorSuggestionItemKind.Method, - detail: '(Macro)', - }, - { - label: '$__timeGroup', - kind: CodeEditorSuggestionItemKind.Method, - detail: '(Macro)', - }, - { - label: '$__unixEpochFilter', - kind: CodeEditorSuggestionItemKind.Method, - detail: '(Macro)', - }, - { - label: '$__unixEpochGroup', - kind: CodeEditorSuggestionItemKind.Method, - detail: '(Macro)', - }, - { - label: '$__schema', - kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${this.state.schema}`, - }, - { - label: '$__table', - kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${this.state.table}`, - }, - { - label: '$__column', - kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${this.state.column}`, - }, - ]; - - if (this.templateSrv) { - this.templateSrv.getVariables().forEach((variable) => { - const label = '$' + variable.name; - let val = this.templateSrv!.replace(label); - if (val === label) { - val = ''; - } - sugs.push({ - label, - kind: CodeEditorSuggestionItemKind.Text, - detail: `(Template Variable) ${val}`, - }); - }); - } - - return sugs; - }; -} diff --git a/src/Suggestions.test.ts b/src/Suggestions.test.ts new file mode 100644 index 00000000..2ff0ea77 --- /dev/null +++ b/src/Suggestions.test.ts @@ -0,0 +1,35 @@ +import { getSuggestions } from 'Suggestions'; +import { mockQuery } from '__mocks__/datasource'; + +const templateSrv = { + getVariables: jest.fn().mockReturnValue([]), + replace: jest.fn(), +}; + +describe('getSuggestions', () => { + const macros = [ + '$__timeEpoch', + '$__timeFilter', + '$__timeFrom', + '$__timeTo', + '$__timeGroup', + '$__unixEpochFilter', + '$__unixEpochGroup', + '$__schema', + '$__table', + '$__column', + ]; + it('should return the list of macros', () => { + expect(getSuggestions({ query: mockQuery, templateSrv }).map((s) => s.label)).toEqual(macros); + }); + + it('should return the list of template variables', () => { + const templateSrv = { + getVariables: jest.fn().mockReturnValue([{ name: 'foo' }, { name: 'bar' }]), + replace: jest.fn(), + }; + expect(getSuggestions({ query: mockQuery, templateSrv }).map((s) => s.label)).toEqual( + macros.concat('$foo', '$bar') + ); + }); +}); diff --git a/src/Suggestions.ts b/src/Suggestions.ts new file mode 100644 index 00000000..4617da93 --- /dev/null +++ b/src/Suggestions.ts @@ -0,0 +1,73 @@ +import { CodeEditorSuggestionItem, CodeEditorSuggestionItemKind } from '@grafana/ui'; +import { TemplateSrv } from '@grafana/runtime'; +import { RedshiftQuery } from './types'; + +export const getSuggestions = ({ templateSrv, query }: { templateSrv: TemplateSrv; query: RedshiftQuery }) => { + const sugs: CodeEditorSuggestionItem[] = [ + { + label: '$__timeEpoch', + kind: CodeEditorSuggestionItemKind.Method, + detail: '(Macro)', + }, + { + label: '$__timeFilter', + kind: CodeEditorSuggestionItemKind.Method, + detail: '(Macro)', + }, + { + label: '$__timeFrom', + kind: CodeEditorSuggestionItemKind.Method, + detail: '(Macro)', + }, + { + label: '$__timeTo', + kind: CodeEditorSuggestionItemKind.Method, + detail: '(Macro)', + }, + { + label: '$__timeGroup', + kind: CodeEditorSuggestionItemKind.Method, + detail: '(Macro)', + }, + { + label: '$__unixEpochFilter', + kind: CodeEditorSuggestionItemKind.Method, + detail: '(Macro)', + }, + { + label: '$__unixEpochGroup', + kind: CodeEditorSuggestionItemKind.Method, + detail: '(Macro)', + }, + { + label: '$__schema', + kind: CodeEditorSuggestionItemKind.Text, + detail: `(Macro) ${query.schema || 'public'}`, + }, + { + label: '$__table', + kind: CodeEditorSuggestionItemKind.Text, + detail: `(Macro) ${query.table}`, + }, + { + label: '$__column', + kind: CodeEditorSuggestionItemKind.Text, + detail: `(Macro) ${query.column}`, + }, + ]; + + templateSrv.getVariables().forEach((variable) => { + const label = '$' + variable.name; + let val = templateSrv.replace(label); + if (val === label) { + val = ''; + } + sugs.push({ + label, + kind: CodeEditorSuggestionItemKind.Text, + detail: `(Template Variable) ${val}`, + }); + }); + + return sugs; +}; diff --git a/src/__mocks__/datasource.ts b/src/__mocks__/datasource.ts index d3269f95..d83704d0 100644 --- a/src/__mocks__/datasource.ts +++ b/src/__mocks__/datasource.ts @@ -1,7 +1,6 @@ import { DataSourcePluginOptionsEditorProps, PluginType } from '@grafana/data'; import { RedshiftDataSourceOptions, RedshiftDataSourceSecureJsonData, RedshiftQuery } from '../types'; import { DataSource } from '../datasource'; -import { SchemaInfo } from 'SchemaInfo'; export const mockDatasource = new DataSource({ id: 1, @@ -64,5 +63,3 @@ export const mockDatasourceOptions: DataSourcePluginOptionsEditorProps< }; export const mockQuery: RedshiftQuery = { rawSQL: 'select * from foo', refId: '', format: 0, fillMode: { mode: 0 } }; - -export const mockSchemaInfo: SchemaInfo = new SchemaInfo(mockDatasource, mockQuery); diff --git a/src/datasource.ts b/src/datasource.ts index 07724a94..77198960 100644 --- a/src/datasource.ts +++ b/src/datasource.ts @@ -1,13 +1,12 @@ -import { DataQueryRequest, DataQueryResponse, DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; +import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data'; import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime'; -import { Observable } from 'rxjs'; import { RedshiftVariableSupport } from 'variables'; import { RedshiftDataSourceOptions, RedshiftQuery } from './types'; export class DataSource extends DataSourceWithBackend { constructor(instanceSettings: DataSourceInstanceSettings) { super(instanceSettings); - this.variables = new RedshiftVariableSupport(); + this.variables = new RedshiftVariableSupport(this); } // This will support annotation queries for 7.2+ @@ -20,14 +19,6 @@ export class DataSource extends DataSourceWithBackend): Observable { - // What is this about? Due to a bug in the templating query system, data source variables doesn't get assigned ref id. - // This leads to bad things to therefore we need to assign a dummy value in case it's undefined. - // The implementation of this method can be removed completely once we upgrade to a version of grafana/data that has this https://github.com/grafana/grafana/pull/35923 - request.targets = request.targets.map((q) => ({ ...q, refId: q.refId ?? 'variable-query' })); - return super.query(request); - } - applyTemplateVariables(query: RedshiftQuery, scopedVars: ScopedVars): RedshiftQuery { const templateSrv = getTemplateSrv(); return { diff --git a/src/variables.ts b/src/variables.ts index 0bf821f8..f545681b 100644 --- a/src/variables.ts +++ b/src/variables.ts @@ -1,14 +1,22 @@ -import { DataSourceVariableSupport, VariableSupportType } from '@grafana/data'; - +import { DataQueryRequest, DataQueryResponse, CustomVariableSupport } from '@grafana/data'; +import { assign } from 'lodash'; +import { QueryCodeEditor } from 'QueryCodeEditor'; +import { Observable } from 'rxjs'; import { DataSource } from './datasource'; -import { RedshiftQuery } from './types'; +import { RedshiftQuery, defaultQuery } from './types'; -export class RedshiftVariableSupport extends DataSourceVariableSupport { - constructor() { +export class RedshiftVariableSupport extends CustomVariableSupport { + constructor(private readonly datasource: DataSource) { super(); + this.datasource = datasource; + this.query = this.query.bind(this); } - getType() { - return VariableSupportType.Datasource; + editor = QueryCodeEditor; + + query(request: DataQueryRequest): Observable { + // fill query params with default data + assign(request.targets, [{ ...defaultQuery, ...request.targets[0], refId: 'A' }]); + return this.datasource.query(request); } } From 9bf7cbcf01ac5949156b82f29e8b24022b4bb41f Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Wed, 6 Oct 2021 18:04:46 +0200 Subject: [PATCH 2/4] fix suggestions --- src/QueryCodeEditor.tsx | 16 ++++++++++++---- src/Suggestions.ts | 19 ++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/QueryCodeEditor.tsx b/src/QueryCodeEditor.tsx index 8e2afe81..88efb7b1 100644 --- a/src/QueryCodeEditor.tsx +++ b/src/QueryCodeEditor.tsx @@ -1,16 +1,20 @@ import { defaults } from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; import { QueryEditorProps } from '@grafana/data'; import { DataSource } from './datasource'; import { defaultQuery, RedshiftDataSourceOptions, RedshiftQuery } from './types'; -import { CodeEditor, InlineFormLabel } from '@grafana/ui'; +import { CodeEditor, CodeEditorSuggestionItem, InlineFormLabel } from '@grafana/ui'; import { getTemplateSrv } from '@grafana/runtime'; import ResourceMacro from 'ResourceMacro'; import { getSuggestions } from 'Suggestions'; type Props = QueryEditorProps; +// getSuggestions result gets cached so we need to reference a var outside the component +// related issue: https://github.com/grafana/grafana/issues/39264 +let suggestions: CodeEditorSuggestionItem[] = []; + export function QueryCodeEditor(props: Props) { const onChange = (value: RedshiftQuery) => { props.onChange(value); @@ -46,6 +50,11 @@ export function QueryCodeEditor(props: Props) { return columns.map((column) => ({ label: column, value: column })).concat({ label: '-- remove --', value: '' }); }; + const { table, column } = props.query; + useEffect(() => { + suggestions = getSuggestions({ table, column, templateSrv: getTemplateSrv() }); + }, [table, column]); + return ( <>
@@ -79,10 +88,9 @@ export function QueryCodeEditor(props: Props) { language={'redshift'} value={rawSQL} onBlur={onRawSqlChange} - // removed onSave due to bug: https://github.com/grafana/grafana/issues/39264 showMiniMap={false} showLineNumbers={true} - getSuggestions={() => getSuggestions({ query: props.query, templateSrv: getTemplateSrv() })} + getSuggestions={() => suggestions} /> ); diff --git a/src/Suggestions.ts b/src/Suggestions.ts index 4617da93..a23d7276 100644 --- a/src/Suggestions.ts +++ b/src/Suggestions.ts @@ -1,8 +1,17 @@ import { CodeEditorSuggestionItem, CodeEditorSuggestionItemKind } from '@grafana/ui'; import { TemplateSrv } from '@grafana/runtime'; -import { RedshiftQuery } from './types'; -export const getSuggestions = ({ templateSrv, query }: { templateSrv: TemplateSrv; query: RedshiftQuery }) => { +export const getSuggestions = ({ + templateSrv, + schema, + table, + column, +}: { + templateSrv: TemplateSrv; + schema?: string; + table?: string; + column?: string; +}) => { const sugs: CodeEditorSuggestionItem[] = [ { label: '$__timeEpoch', @@ -42,17 +51,17 @@ export const getSuggestions = ({ templateSrv, query }: { templateSrv: TemplateSr { label: '$__schema', kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${query.schema || 'public'}`, + detail: `(Macro) ${schema || 'public'}`, }, { label: '$__table', kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${query.table}`, + detail: `(Macro) ${table}`, }, { label: '$__column', kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${query.column}`, + detail: `(Macro) ${column}`, }, ]; From d64e0e09cf4b71958f45e8ad2f9c3ee6f7c5d261 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 7 Oct 2021 09:29:59 +0200 Subject: [PATCH 3/4] Revert "fix suggestions" This reverts commit 9bf7cbcf01ac5949156b82f29e8b24022b4bb41f. --- src/QueryCodeEditor.tsx | 16 ++++------------ src/Suggestions.ts | 19 +++++-------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/src/QueryCodeEditor.tsx b/src/QueryCodeEditor.tsx index 88efb7b1..8e2afe81 100644 --- a/src/QueryCodeEditor.tsx +++ b/src/QueryCodeEditor.tsx @@ -1,20 +1,16 @@ import { defaults } from 'lodash'; -import React, { useEffect } from 'react'; +import React from 'react'; import { QueryEditorProps } from '@grafana/data'; import { DataSource } from './datasource'; import { defaultQuery, RedshiftDataSourceOptions, RedshiftQuery } from './types'; -import { CodeEditor, CodeEditorSuggestionItem, InlineFormLabel } from '@grafana/ui'; +import { CodeEditor, InlineFormLabel } from '@grafana/ui'; import { getTemplateSrv } from '@grafana/runtime'; import ResourceMacro from 'ResourceMacro'; import { getSuggestions } from 'Suggestions'; type Props = QueryEditorProps; -// getSuggestions result gets cached so we need to reference a var outside the component -// related issue: https://github.com/grafana/grafana/issues/39264 -let suggestions: CodeEditorSuggestionItem[] = []; - export function QueryCodeEditor(props: Props) { const onChange = (value: RedshiftQuery) => { props.onChange(value); @@ -50,11 +46,6 @@ export function QueryCodeEditor(props: Props) { return columns.map((column) => ({ label: column, value: column })).concat({ label: '-- remove --', value: '' }); }; - const { table, column } = props.query; - useEffect(() => { - suggestions = getSuggestions({ table, column, templateSrv: getTemplateSrv() }); - }, [table, column]); - return ( <>
@@ -88,9 +79,10 @@ export function QueryCodeEditor(props: Props) { language={'redshift'} value={rawSQL} onBlur={onRawSqlChange} + // removed onSave due to bug: https://github.com/grafana/grafana/issues/39264 showMiniMap={false} showLineNumbers={true} - getSuggestions={() => suggestions} + getSuggestions={() => getSuggestions({ query: props.query, templateSrv: getTemplateSrv() })} /> ); diff --git a/src/Suggestions.ts b/src/Suggestions.ts index a23d7276..4617da93 100644 --- a/src/Suggestions.ts +++ b/src/Suggestions.ts @@ -1,17 +1,8 @@ import { CodeEditorSuggestionItem, CodeEditorSuggestionItemKind } from '@grafana/ui'; import { TemplateSrv } from '@grafana/runtime'; +import { RedshiftQuery } from './types'; -export const getSuggestions = ({ - templateSrv, - schema, - table, - column, -}: { - templateSrv: TemplateSrv; - schema?: string; - table?: string; - column?: string; -}) => { +export const getSuggestions = ({ templateSrv, query }: { templateSrv: TemplateSrv; query: RedshiftQuery }) => { const sugs: CodeEditorSuggestionItem[] = [ { label: '$__timeEpoch', @@ -51,17 +42,17 @@ export const getSuggestions = ({ { label: '$__schema', kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${schema || 'public'}`, + detail: `(Macro) ${query.schema || 'public'}`, }, { label: '$__table', kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${table}`, + detail: `(Macro) ${query.table}`, }, { label: '$__column', kind: CodeEditorSuggestionItemKind.Text, - detail: `(Macro) ${column}`, + detail: `(Macro) ${query.column}`, }, ]; From c4617da862b27f9a43f44299ac1022b6541b85b3 Mon Sep 17 00:00:00 2001 From: Andres Martinez Gotor Date: Thu, 7 Oct 2021 17:51:24 +0200 Subject: [PATCH 4/4] review --- src/QueryEditor.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/QueryEditor.tsx b/src/QueryEditor.tsx index 0df844e1..e7db0e18 100644 --- a/src/QueryEditor.tsx +++ b/src/QueryEditor.tsx @@ -1,5 +1,3 @@ -import { defaults } from 'lodash'; - import React, { useState } from 'react'; import { QueryEditorProps } from '@grafana/data'; import { DataSource } from './datasource'; @@ -29,7 +27,7 @@ export function QueryEditor(props: Props) { setFillValue(currentTarget.valueAsNumber); }; - const { format, fillMode } = defaults(props.query, defaultQuery); + const { format, fillMode } = { ...props.query, ...defaultQuery }; return ( <>