diff --git a/.betterer.results b/.betterer.results index 642e8bcaffa..0acd2a6c43e 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6510,6 +6510,20 @@ exports[`better eslint`] = { [173, 23, 47, "Do not use any type assertions.", "3309878203"], [195, 43, 45, "Do not use any type assertions.", "15355460"] ], + "public/app/features/plugins/sql/components/visual-query-builder/AwesomeQueryBuilder.tsx:760854115": [ + [175, 24, 63, "Do not use any type assertions.", "2252455532"], + [198, 13, 47, "Do not use any type assertions.", "2763495851"], + [253, 30, 3, "Unexpected any. Specify a different type.", "193409811"] + ], + "public/app/features/plugins/sql/datasource/SqlDatasource.ts:178321817": [ + [124, 33, 3, "Unexpected any. Specify a different type.", "193409811"], + [159, 56, 3, "Unexpected any. Specify a different type.", "193409811"], + [177, 19, 3, "Unexpected any. Specify a different type.", "193409811"], + [182, 57, 3, "Unexpected any. Specify a different type.", "193409811"], + [184, 18, 135, "Do not use any type assertions.", "3270957200"], + [194, 28, 3, "Unexpected any. Specify a different type.", "193409811"], + [225, 33, 3, "Unexpected any. Specify a different type.", "193409811"] + ], "public/app/features/plugins/tests/datasource_srv.test.ts:2399414445": [ [10, 19, 3, "Unexpected any. Specify a different type.", "193409811"], [44, 48, 21, "Do not use any type assertions.", "932413114"], diff --git a/package.json b/package.json index f66a27d6340..9184867d9f1 100644 --- a/package.json +++ b/package.json @@ -354,6 +354,7 @@ "rc-time-picker": "3.7.3", "re-resizable": "6.9.9", "react": "17.0.2", + "react-awesome-query-builder": "^5.1.2", "react-beautiful-dnd": "13.1.0", "react-diff-viewer": "^3.1.1", "react-dom": "17.0.2", diff --git a/public/app/features/plugins/sql/components/ConfirmModal.tsx b/public/app/features/plugins/sql/components/ConfirmModal.tsx new file mode 100644 index 00000000000..6ee12a6ce7f --- /dev/null +++ b/public/app/features/plugins/sql/components/ConfirmModal.tsx @@ -0,0 +1,51 @@ +import React, { useRef, useEffect } from 'react'; + +import { Button, Icon, Modal } from '@grafana/ui'; + +type ConfirmModalProps = { + isOpen: boolean; + onCancel?: () => void; + onDiscard?: () => void; + onCopy?: () => void; +}; +export function ConfirmModal({ isOpen, onCancel, onDiscard, onCopy }: ConfirmModalProps) { + const buttonRef = useRef(null); + + // Moved from grafana/ui + useEffect(() => { + // for some reason autoFocus property did no work on this button, but this does + if (isOpen) { + buttonRef.current?.focus(); + } + }, [isOpen]); + + return ( + + + Warning + + } + onDismiss={onCancel} + isOpen={isOpen} + > +

+ Builder mode does not display changes made in code. The query builder will display the last changes you made in + builder mode. +

+

Do you want to copy your code to the clipboard?

+ + + + + +
+ ); +} diff --git a/public/app/features/plugins/sql/components/DatasetSelector.tsx b/public/app/features/plugins/sql/components/DatasetSelector.tsx new file mode 100644 index 00000000000..031cee09594 --- /dev/null +++ b/public/app/features/plugins/sql/components/DatasetSelector.tsx @@ -0,0 +1,61 @@ +import React, { useEffect } from 'react'; +import { useAsync } from 'react-use'; + +import { SelectableValue } from '@grafana/data'; +import { Select } from '@grafana/ui'; + +import { DB, ResourceSelectorProps, toOption } from '../types'; + +interface DatasetSelectorProps extends ResourceSelectorProps { + db: DB; + value: string | null; + applyDefault?: boolean; + disabled?: boolean; + onChange: (v: SelectableValue) => void; +} + +export const DatasetSelector: React.FC = ({ + db, + value, + onChange, + disabled, + className, + applyDefault, +}) => { + const state = useAsync(async () => { + const datasets = await db.datasets(); + return datasets.map(toOption); + }, []); + + useEffect(() => { + if (!applyDefault) { + return; + } + // Set default dataset when values are fetched + if (!value) { + if (state.value && state.value[0]) { + onChange(state.value[0]); + } + } else { + if (state.value && state.value.find((v) => v.value === value) === undefined) { + // if value is set and newly fetched values does not contain selected value + if (state.value.length > 0) { + onChange(state.value[0]); + } + } + } + }, [state.value, value, applyDefault, onChange]); + + return ( + + + } + > + + + + {editorMode === EditorMode.Builder && ( + <> + + ev.target instanceof HTMLInputElement && + onQueryRowChange({ ...queryRowFilter, filter: ev.target.checked }) + } + /> + + + ev.target instanceof HTMLInputElement && + onQueryRowChange({ ...queryRowFilter, group: ev.target.checked }) + } + /> + + + ev.target instanceof HTMLInputElement && + onQueryRowChange({ ...queryRowFilter, order: ev.target.checked }) + } + /> + + + ev.target instanceof HTMLInputElement && + onQueryRowChange({ ...queryRowFilter, preview: ev.target.checked }) + } + /> + + )} + + + + {isQueryRunnable ? ( + + ) : ( + + Your query is invalid. Check below for details.
+ However, you can still run this query. + + } + placement="top" + > + +
+ )} + + + + { + setShowConfirm(false); + copyToClipboard(query.rawSql!); + onChange({ + ...query, + rawSql: toRawSql(query), + editorMode: EditorMode.Builder, + }); + }} + onDiscard={() => { + setShowConfirm(false); + onChange({ + ...query, + rawSql: toRawSql(query), + editorMode: EditorMode.Builder, + }); + }} + onCancel={() => setShowConfirm(false)} + /> + + + {editorMode === EditorMode.Builder && ( + <> + + + + + + + + + + + + + )} + + ); +} diff --git a/public/app/features/plugins/sql/components/TableSelector.tsx b/public/app/features/plugins/sql/components/TableSelector.tsx new file mode 100644 index 00000000000..65171e2b918 --- /dev/null +++ b/public/app/features/plugins/sql/components/TableSelector.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { useAsync } from 'react-use'; + +import { SelectableValue, toOption } from '@grafana/data'; +import { Select } from '@grafana/ui'; + +import { QueryWithDefaults } from '../defaults'; +import { DB, ResourceSelectorProps } from '../types'; + +interface TableSelectorProps extends ResourceSelectorProps { + db: DB; + value: string | null; + query: QueryWithDefaults; + onChange: (v: SelectableValue) => void; +} + +export const TableSelector: React.FC = ({ db, query, value, className, onChange }) => { + const state = useAsync(async () => { + if (!query.dataset) { + return []; + } + const tables = await db.tables(query.dataset); + return tables.map(toOption); + }, [query.dataset]); + + return ( + props?.setValue(e.currentTarget.value)} + /> + ); + }, + }, + number: { + ...BasicConfig.widgets.number, + factory: function NumberInput(props) { + return ( + props?.setValue(Number.parseInt(e.currentTarget.value, 10))} + /> + ); + }, + }, + datetime: { + ...BasicConfig.widgets.datetime, + factory: function DateTimeInput(props) { + return ( + { + props?.setValue(e.format(BasicConfig.widgets.datetime.valueFormat)); + }} + date={dateTime(props?.value).utc()} + /> + ); + }, + }, +}; + +export const settings: Settings = { + ...BasicConfig.settings, + canRegroup: false, + maxNesting: 1, + canReorder: false, + showNot: false, + addRuleLabel: buttonLabels.add, + deleteLabel: buttonLabels.remove, + renderConjs: function Conjunctions(conjProps) { + return ( + { + // @ts-ignore + const icon = fields[f.key].mainWidgetProps?.customProps?.icon; + return { + label: f.label, + value: f.key, + icon, + }; + })} + value={fieldProps?.selectedKey} + onChange={(val) => { + fieldProps?.setField(val.label!); + }} + /> + ); + }, + renderButton: function RAQBButton(buttonProps) { + return ( +