Revert SQL data sources migration for 9.1 (#53510)

* Revert "Core code editor/builder components (#52421)"

This reverts commit de956fc3d8.

* Revert "Chore: typos in MySQL/PostgreSQL ConfigurationEditor (#52735)"

This reverts commit 7eabd7c83a.

* Revert "mysql query editor - angular to react (#50343)"

This reverts commit 53933972b6.

* Revert "MSSQL: Migrate to React (#51765)"

This reverts commit 35d98104ad.

* Revert "SQL: Migrate (MS/My/Postgres)SQL configuration pages from Angular to React (#51891)"

This reverts commit 9498ee3d54.

* Revert "SQL: Fix couple of issues in SQLDatasource (#52080)"

This reverts commit 4155dc8eca.

* Revert "sql plugins - angular to react - base sql datasource (#51655)"

This reverts commit fa560d96b6.

* These changes are okay

* These should also be fine + betterer update

* Change these back
This commit is contained in:
Zoltán Bedi
2022-08-12 09:36:13 +02:00
committed by GitHub
parent 1237290a9d
commit 2c7e77b124
220 changed files with 3274 additions and 11482 deletions
+175 -36
View File
@@ -5376,39 +5376,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"]
],
"public/app/features/plugins/sql/components/query-editor-raw/SQLEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"]
],
"public/app/features/plugins/sql/components/visual-query-builder/AwesomeQueryBuilder.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/plugins/sql/mocks/Monaco.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/plugins/sql/mocks/queries/singleLineFullQuery.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/plugins/sql/standardSql/definition.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/plugins/sql/test-utils/statementPosition.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"]
],
"public/app/features/plugins/sql/utils/debugger.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/plugins/tests/datasource_srv.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@@ -7613,15 +7580,176 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/mssql/config_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mssql/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"]
],
"public/app/plugins/datasource/mssql/module.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mssql/query_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/datasource/mssql/response_parser.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/mssql/specs/datasource.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
],
"public/app/plugins/datasource/mssql/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/plugins/datasource/mysql/datasource.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"]
],
"public/app/plugins/datasource/mysql/meta_query.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mysql/module.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/plugins/datasource/mysql/mysql_query_model.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"]
],
"public/app/plugins/datasource/mysql/query_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Unexpected any. Specify a different type.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Unexpected any. Specify a different type.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Do not use any type assertions.", "24"],
[0, 0, 0, "Do not use any type assertions.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
[0, 0, 0, "Unexpected any. Specify a different type.", "29"],
[0, 0, 0, "Unexpected any. Specify a different type.", "30"],
[0, 0, 0, "Unexpected any. Specify a different type.", "31"],
[0, 0, 0, "Unexpected any. Specify a different type.", "32"],
[0, 0, 0, "Unexpected any. Specify a different type.", "33"],
[0, 0, 0, "Unexpected any. Specify a different type.", "34"],
[0, 0, 0, "Unexpected any. Specify a different type.", "35"],
[0, 0, 0, "Unexpected any. Specify a different type.", "36"],
[0, 0, 0, "Unexpected any. Specify a different type.", "37"],
[0, 0, 0, "Unexpected any. Specify a different type.", "38"],
[0, 0, 0, "Unexpected any. Specify a different type.", "39"],
[0, 0, 0, "Unexpected any. Specify a different type.", "40"],
[0, 0, 0, "Unexpected any. Specify a different type.", "41"],
[0, 0, 0, "Unexpected any. Specify a different type.", "42"],
[0, 0, 0, "Unexpected any. Specify a different type.", "43"],
[0, 0, 0, "Unexpected any. Specify a different type.", "44"],
[0, 0, 0, "Unexpected any. Specify a different type.", "45"],
[0, 0, 0, "Unexpected any. Specify a different type.", "46"],
[0, 0, 0, "Unexpected any. Specify a different type.", "47"],
[0, 0, 0, "Unexpected any. Specify a different type.", "48"],
[0, 0, 0, "Unexpected any. Specify a different type.", "49"],
[0, 0, 0, "Unexpected any. Specify a different type.", "50"],
[0, 0, 0, "Unexpected any. Specify a different type.", "51"],
[0, 0, 0, "Unexpected any. Specify a different type.", "52"],
[0, 0, 0, "Unexpected any. Specify a different type.", "53"],
[0, 0, 0, "Unexpected any. Specify a different type.", "54"]
],
"public/app/plugins/datasource/mysql/response_parser.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/datasource/mysql/specs/datasource.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/plugins/datasource/mysql/sql_part.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/datasource/mysql/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/plugins/datasource/opentsdb/datasource.d.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
@@ -7726,6 +7854,15 @@ exports[`better eslint`] = {
"public/app/plugins/datasource/opentsdb/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/datasource/postgres/config_ctrl.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
],
"public/app/plugins/datasource/postgres/datasource.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
@@ -7746,7 +7883,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"]
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"]
],
"public/app/plugins/datasource/postgres/module.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
+1 -3
View File
@@ -87,7 +87,6 @@
"@babel/preset-env": "7.18.9",
"@babel/preset-react": "7.18.6",
"@babel/preset-typescript": "7.18.6",
"@babel/runtime": "7.18.9",
"@betterer/betterer": "5.3.7",
"@betterer/cli": "5.3.7",
"@betterer/eslint": "5.3.7",
@@ -254,6 +253,7 @@
"@grafana/aws-sdk": "0.0.37",
"@grafana/data": "workspace:*",
"@grafana/e2e-selectors": "workspace:*",
"@grafana/experimental": "^0.0.2-canary.32",
"@grafana/google-sdk": "0.0.3",
"@grafana/lezer-logql": "0.0.14",
"@grafana/runtime": "workspace:*",
@@ -352,7 +352,6 @@
"rc-tree": "5.6.6",
"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",
@@ -388,7 +387,6 @@
"semver": "7.3.7",
"slate": "0.47.8",
"slate-plain-serializer": "0.7.10",
"sql-formatter-plus": "^1.3.6",
"symbol-observable": "4.0.0",
"test": "link:./public/test",
"tether-drop": "https://github.com/torkelo/drop",
@@ -1,23 +0,0 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2, stylesFactory } from '../../themes';
import { Button, ButtonProps } from '../Button';
interface AccessoryButtonProps extends ButtonProps {}
export const AccessoryButton: React.FC<AccessoryButtonProps> = ({ className, ...props }) => {
const theme = useTheme2();
const styles = getButtonStyles(theme);
return <Button {...props} className={cx(className, styles.button)} />;
};
const getButtonStyles = stylesFactory((theme: GrafanaTheme2) => ({
button: css({
paddingLeft: theme.spacing(3 / 2),
paddingRight: theme.spacing(3 / 2),
}),
}));
@@ -1,79 +0,0 @@
import { css } from '@emotion/css';
import React, { ComponentProps } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
import { ReactUtils } from '../../utils';
import { Field } from '../Forms/Field';
import { Icon } from '../Icon/Icon';
import { PopoverContent, Tooltip } from '../Tooltip';
import { Space } from './Space';
interface EditorFieldProps extends ComponentProps<typeof Field> {
label: string;
children: React.ReactElement;
width?: number | string;
optional?: boolean;
tooltip?: PopoverContent;
}
export const EditorField: React.FC<EditorFieldProps> = (props) => {
const { label, optional, tooltip, children, width, ...fieldProps } = props;
const theme = useTheme2();
const styles = getStyles(theme, width);
// Null check for backward compatibility
const childInputId = fieldProps?.htmlFor || ReactUtils?.getChildId(children);
const labelEl = (
<>
<label className={styles.label} htmlFor={childInputId}>
{label}
{optional && <span className={styles.optional}> - optional</span>}
{tooltip && (
<Tooltip placement="top" content={tooltip} theme="info">
<Icon name="info-circle" size="sm" className={styles.icon} />
</Tooltip>
)}
</label>
<Space v={0.5} />
</>
);
return (
<div className={styles.root}>
<Field className={styles.field} label={labelEl} {...fieldProps}>
{children}
</Field>
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme2, width?: number | string) => {
return {
root: css({
minWidth: theme.spacing(width ?? 0),
}),
label: css({
fontSize: 12,
fontWeight: theme.typography.fontWeightMedium,
}),
optional: css({
fontStyle: 'italic',
color: theme.colors.text.secondary,
}),
field: css({
marginBottom: 0, // GrafanaUI/Field has a bottom margin which we must remove
}),
icon: css({
color: theme.colors.text.secondary,
marginLeft: theme.spacing(1),
':hover': {
color: theme.colors.text.primary,
},
}),
};
});
@@ -1,9 +0,0 @@
import React from 'react';
import { Stack } from './Stack';
interface EditorFieldGroupProps {}
export const EditorFieldGroup: React.FC<EditorFieldGroupProps> = ({ children }) => {
return <Stack gap={1}>{children}</Stack>;
};
@@ -1,25 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
interface EditorHeaderProps {}
export const EditorHeader: React.FC<EditorHeaderProps> = ({ children }) => {
const theme = useTheme2();
const styles = getStyles(theme);
return <div className={styles.root}>{children}</div>;
};
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
root: css({
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: theme.spacing(3),
minHeight: theme.spacing(4),
}),
}));
@@ -1,49 +0,0 @@
import React from 'react';
import { Button } from '../Button';
import { Stack } from './Stack';
interface EditorListProps<T> {
items: Array<Partial<T>>;
renderItem: (
item: Partial<T>,
onChangeItem: (item: Partial<T>) => void,
onDeleteItem: () => void
) => React.ReactElement;
onChange: (items: Array<Partial<T>>) => void;
}
export function EditorList<T>({ items, renderItem, onChange }: EditorListProps<T>) {
const onAddItem = () => {
const newItems = [...items, {}];
onChange(newItems);
};
const onChangeItem = (itemIndex: number, newItem: Partial<T>) => {
const newItems = [...items];
newItems[itemIndex] = newItem;
onChange(newItems);
};
const onDeleteItem = (itemIndex: number) => {
const newItems = [...items];
newItems.splice(itemIndex, 1);
onChange(newItems);
};
return (
<Stack>
{items.map((item, index) => (
<div key={index}>
{renderItem(
item,
(newItem) => onChangeItem(index, newItem),
() => onDeleteItem(index)
)}
</div>
))}
<Button onClick={onAddItem} variant="secondary" size="md" icon="plus" aria-label="Add" type="button" />
</Stack>
);
}
@@ -1,30 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../themes';
import { Stack } from './Stack';
interface EditorRowProps {}
export const EditorRow: React.FC<EditorRowProps> = ({ children }) => {
const styles = useStyles2(getStyles);
return (
<div className={styles.root}>
<Stack gap={2}>{children}</Stack>
</div>
);
};
const getStyles = (theme: GrafanaTheme2) => {
return {
root: css({
padding: theme.spacing(1),
backgroundColor: theme.colors.background.secondary,
borderRadius: theme.shape.borderRadius(1),
}),
};
};
@@ -1,13 +0,0 @@
import React from 'react';
import { Stack } from './Stack';
interface EditorRowsProps {}
export const EditorRows: React.FC<EditorRowsProps> = ({ children }) => {
return (
<Stack gap={0.5} direction="column">
{children}
</Stack>
);
};
@@ -1,25 +0,0 @@
import { css } from '@emotion/css';
import React, { ComponentProps } from 'react';
import { Switch } from '../Switch/Switch';
// Wrapper component around <Switch /> that properly aligns it in <EditorField />
export const EditorSwitch: React.FC<ComponentProps<typeof Switch>> = (props) => {
const styles = getStyles();
return (
<div className={styles.switch}>
<Switch {...props} />
</div>
);
};
const getStyles = () => {
return {
switch: css({
display: 'flex',
alignItems: 'center',
minHeight: 30,
}),
};
};
@@ -1,10 +0,0 @@
import React from 'react';
interface FlexItemProps {
grow?: number;
shrink?: number;
}
export const FlexItem: React.FC<FlexItemProps> = ({ grow, shrink }) => {
return <div style={{ display: 'block', flexGrow: grow, flexShrink: shrink }} />;
};
@@ -1,89 +0,0 @@
import { css, cx } from '@emotion/css';
import React, { useState } from 'react';
import { GroupBase } from 'react-select';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
import { Select } from '../Select/Select';
import { SelectContainerProps, SelectContainer as BaseSelectContainer } from '../Select/SelectContainer';
import { SelectCommonProps } from '../Select/types';
interface InlineSelectProps<T> extends SelectCommonProps<T> {
label?: string;
}
export function InlineSelect<T>({ label: labelProp, ...props }: InlineSelectProps<T>) {
const theme = useTheme2();
const [id] = useState(() => Math.random().toString(16).slice(2));
const styles = getSelectStyles(theme);
const components = {
SelectContainer,
ValueContainer,
SingleValue: ValueContainer,
};
return (
<div className={styles.root}>
{labelProp && (
<label className={styles.label} htmlFor={id}>
{labelProp}
{':'}&nbsp;
</label>
)}
{/* @ts-ignore */}
<Select openMenuOnFocus inputId={id} {...props} components={components} />
</div>
);
}
const SelectContainer = <Option, isMulti extends boolean, Group extends GroupBase<Option>>(
props: SelectContainerProps<Option, isMulti, Group>
) => {
const { children } = props;
const theme = useTheme2();
const styles = getSelectStyles(theme);
return (
<BaseSelectContainer {...props} className={cx(props.className, styles.container)}>
{children}
</BaseSelectContainer>
);
};
const ValueContainer = <Option, isMulti extends boolean, Group extends GroupBase<Option>>(
props: SelectContainerProps<Option, isMulti, Group>
) => {
const { className, children } = props;
const theme = useTheme2();
const styles = getSelectStyles(theme);
return <div className={cx(className, styles.valueContainer)}>{children}</div>;
};
const getSelectStyles = stylesFactory((theme: GrafanaTheme2) => ({
root: css({
display: 'flex',
fontSize: 12,
alignItems: 'center',
}),
label: css({
color: theme.colors.text.secondary,
whiteSpace: 'nowrap',
}),
container: css({
background: 'none',
borderColor: 'transparent',
}),
valueContainer: css({
display: 'flex',
alignItems: 'center',
flex: 'initial',
color: theme.colors.text.secondary,
fontSize: 12,
}),
}));
@@ -1,54 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
interface InputGroupProps {}
export const InputGroup: React.FC<InputGroupProps> = ({ children }) => {
const theme = useTheme2();
const styles = useStyles(theme);
return <div className={styles.root}>{children}</div>;
};
const useStyles = stylesFactory((theme: GrafanaTheme2) => ({
root: css({
display: 'flex',
// Style the direct children of the component
'> *': {
'&:not(:first-child)': {
// Negative margin hides the double-border on adjacent selects
marginLeft: -1,
},
'&:first-child': {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
},
'&:last-child': {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
'&:not(:first-child):not(:last-child)': {
borderRadius: 0,
},
//
position: 'relative',
zIndex: 1,
'&:hover': {
zIndex: 2,
},
'&:focus-within': {
zIndex: 2,
},
},
}),
}));
@@ -1,40 +0,0 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
export interface SpaceProps {
v?: number;
h?: number;
layout?: 'block' | 'inline';
}
export const Space = (props: SpaceProps) => {
const theme = useTheme2();
const styles = getStyles(theme, props);
return <span className={cx(styles.wrapper)} />;
};
Space.defaultProps = {
v: 0,
h: 0,
layout: 'block',
};
const getStyles = stylesFactory((theme: GrafanaTheme2, props: SpaceProps) => ({
wrapper: css([
{
paddingRight: theme.spacing(props.h ?? 0),
paddingBottom: theme.spacing(props.v ?? 0),
},
props.layout === 'inline' && {
display: 'inline-block',
},
props.layout === 'block' && {
display: 'block',
},
]),
}));
@@ -1,30 +0,0 @@
import { css } from '@emotion/css';
import React, { CSSProperties } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { stylesFactory, useTheme2 } from '../../themes';
interface StackProps {
direction?: CSSProperties['flexDirection'];
alignItems?: CSSProperties['alignItems'];
wrap?: boolean;
gap?: number;
}
export const Stack: React.FC<StackProps> = ({ children, ...props }) => {
const theme = useTheme2();
const styles = useStyles(theme, props);
return <div className={styles.root}>{children}</div>;
};
const useStyles = stylesFactory((theme: GrafanaTheme2, props: StackProps) => ({
root: css({
display: 'flex',
flexDirection: props.direction ?? 'row',
flexWrap: props.wrap ?? true ? 'wrap' : undefined,
alignItems: props.alignItems,
gap: theme.spacing(props.gap ?? 2),
}),
}));
@@ -1,13 +0,0 @@
export { AccessoryButton } from './AccessoryButton';
export { EditorFieldGroup } from './EditorFieldGroup';
export { EditorHeader } from './EditorHeader';
export { EditorField } from './EditorField';
export { EditorRow } from './EditorRow';
export { EditorList } from './EditorList';
export { EditorRows } from './EditorRows';
export { EditorSwitch } from './EditorSwitch';
export { FlexItem } from './FlexItem';
export { Stack } from './Stack';
export { InlineSelect } from './InlineSelect';
export { InputGroup } from './InputGroup';
export { Space } from './Space';
@@ -267,4 +267,3 @@ export * from './PanelChrome/types';
export { EmotionPerfTest } from './ThemeDemos/EmotionPerfTest';
export { Label as BrowserLabel } from './BrowserLabel/Label';
export { PanelContainer } from './PanelContainer/PanelContainer';
export * from './QueryEditor';
@@ -5,7 +5,8 @@ import { useDispatch } from 'react-redux';
import { useDebounce } from 'react-use';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Card, FilterInput, Icon, Pagination, Select, Stack, TagList, useStyles2 } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { Card, FilterInput, Icon, Pagination, Select, TagList, useStyles2 } from '@grafana/ui';
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
import { getQueryParamValue } from 'app/core/utils/query';
import { FolderState } from 'app/types';
@@ -2,7 +2,8 @@ import { css } from '@emotion/css';
import React, { FormEvent } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Label, Tooltip, Input, Icon, useStyles2, Stack } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { Label, Tooltip, Input, Icon, useStyles2 } from '@grafana/ui';
interface Props {
className?: string;
@@ -4,7 +4,8 @@ import React, { useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Field, Input, InputControl, Label, Tooltip, Icon, Stack } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { useStyles2, Field, Input, InputControl, Label, Tooltip, Icon } from '@grafana/ui';
import { FolderPickerFilter } from 'app/core/components/Select/FolderPicker';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardSearchHit } from 'app/features/search/types';
@@ -3,7 +3,8 @@ import { isEmpty } from 'lodash';
import React, { FC } from 'react';
import { GrafanaTheme2 } from '@grafana/data/src';
import { Stack, useStyles2 } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { useStyles2 } from '@grafana/ui';
import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler';
import { RuleFormType } from '../../../types/rule-form';
@@ -3,8 +3,9 @@ import { debounce } from 'lodash';
import React, { FormEvent, useState } from 'react';
import { DataSourceInstanceSettings, GrafanaTheme, SelectableValue } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { DataSourcePicker } from '@grafana/runtime';
import { Button, Field, Icon, Input, Label, RadioButtonGroup, Stack, Tooltip, useStyles } from '@grafana/ui';
import { Button, Field, Icon, Input, Label, RadioButtonGroup, Tooltip, useStyles } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';
@@ -3,7 +3,8 @@ import { groupBy } from 'lodash';
import React, { FC, FormEvent, useCallback, useState } from 'react';
import { AlertState, dateTimeFormat, GrafanaTheme2 } from '@grafana/data';
import { Alert, Field, Icon, Input, Label, LoadingPlaceholder, Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { Alert, Field, Icon, Input, Label, LoadingPlaceholder, Tooltip, useStyles2 } from '@grafana/ui';
import { StateHistoryItem, StateHistoryItemData } from 'app/types/unified-alerting';
import { GrafanaAlertStateWithReason, PromAlertingRuleState } from 'app/types/unified-alerting-dto';
@@ -3,7 +3,8 @@ import { debounce, uniqueId } from 'lodash';
import React, { FormEvent, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Label, Icon, Input, Tooltip, RadioButtonGroup, useStyles2, Button, Field, Stack } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { Label, Icon, Input, Tooltip, RadioButtonGroup, useStyles2, Button, Field } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { SilenceState } from 'app/plugins/datasource/alertmanager/types';
@@ -3,7 +3,8 @@ import React, { FC, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { GrafanaTheme2, dateMath } from '@grafana/data';
import { Icon, useStyles2, Link, Button, Stack } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { Icon, useStyles2, Link, Button } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types';
@@ -1,7 +1,8 @@
import React, { useMemo, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { Button, Checkbox, Form, Stack, TextArea } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { Button, Checkbox, Form, TextArea } from '@grafana/ui';
import { DashboardModel } from 'app/features/dashboard/state';
import { SaveDashboardData, SaveDashboardOptions } from '../types';
@@ -3,7 +3,8 @@ import { saveAs } from 'file-saver';
import React, { useCallback, useState } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { Button, ClipboardButton, HorizontalGroup, Stack, stylesFactory, TextArea, useTheme } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { Button, ClipboardButton, HorizontalGroup, stylesFactory, TextArea, useTheme } from '@grafana/ui';
import { SaveDashboardFormProps } from '../types';
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react';
import { useAsync } from 'react-use';
import { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental';
import { locationService } from '@grafana/runtime';
import {
Button,
@@ -12,7 +13,6 @@ import {
Input,
RadioButtonGroup,
Spinner,
Stack,
TextArea,
} from '@grafana/ui';
import { getGrafanaStorage } from 'app/features/storage/storage';
@@ -3,7 +3,8 @@ import React, { ChangeEvent, FC } from 'react';
import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Icon, InlineField, Stack, TextArea, useStyles2 } from '@grafana/ui';
import { Stack } from '@grafana/experimental';
import { Button, Icon, InlineField, TextArea, useStyles2 } from '@grafana/ui';
import { ExpressionQuery } from '../types';
@@ -4,8 +4,9 @@ import { Subscription } from 'rxjs';
import { DataFrame } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental';
import { config, RefreshEvent } from '@grafana/runtime';
import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder, Stack } from '@grafana/ui';
import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder } from '@grafana/ui';
import { backendSrv } from 'app/core/services/backend_srv';
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
import { PanelModel } from 'app/features/dashboard/state';
+2 -12
View File
@@ -1,19 +1,9 @@
import React from 'react';
import { locationUtil } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { locationService } from '@grafana/runtime';
import {
Button,
LinkButton,
Input,
Switch,
RadioButtonGroup,
Form,
Field,
InputControl,
FieldSet,
Stack,
} from '@grafana/ui';
import { Button, LinkButton, Input, Switch, RadioButtonGroup, Form, Field, InputControl, FieldSet } from '@grafana/ui';
import { getConfig } from 'app/core/config';
import { OrgRole, useDispatch } from 'app/types';
@@ -1,51 +0,0 @@
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<HTMLButtonElement>(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 (
<Modal
title={
<div className="modal-header-title">
<Icon name="exclamation-triangle" size="lg" />
<span className="p-l-1">Warning</span>
</div>
}
onDismiss={onCancel}
isOpen={isOpen}
>
<p>
Builder mode does not display changes made in code. The query builder will display the last changes you made in
builder mode.
</p>
<p>Do you want to copy your code to the clipboard?</p>
<Modal.ButtonRow>
<Button type="button" variant="secondary" onClick={onCancel} fill="outline">
Cancel
</Button>
<Button variant="destructive" type="button" onClick={onDiscard} ref={buttonRef}>
Discard code and switch
</Button>
<Button variant="primary" onClick={onCopy}>
Copy code and switch
</Button>
</Modal.ButtonRow>
</Modal>
);
}
@@ -1,61 +0,0 @@
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<DatasetSelectorProps> = ({
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 (
<Select
className={className}
aria-label="Dataset selector"
value={value}
options={state.value}
onChange={onChange}
disabled={disabled}
isLoading={state.loading}
menuShouldPortal={true}
/>
);
};
@@ -1,25 +0,0 @@
import React from 'react';
type Props = {
fallBackComponent?: React.ReactNode;
};
export class ErrorBoundary extends React.Component<Props, { hasError: boolean }> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
const FallBack = this.props.fallBackComponent || <div>Error</div>;
return FallBack;
}
return this.props.children;
}
}
@@ -1,119 +0,0 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useAsync } from 'react-use';
import { QueryEditorProps } from '@grafana/data';
import { Space } from '@grafana/ui';
import { SqlDatasource } from '../datasource/SqlDatasource';
import { applyQueryDefaults } from '../defaults';
import { SQLQuery, QueryRowFilter, SQLOptions, EditorMode } from '../types';
import { haveColumns } from '../utils/sql.utils';
import { QueryHeader } from './QueryHeader';
import { RawEditor } from './query-editor-raw/RawEditor';
import { VisualEditor } from './visual-query-builder/VisualEditor';
type Props = QueryEditorProps<SqlDatasource, SQLQuery, SQLOptions>;
export function SqlQueryEditor({ datasource, query, onChange, onRunQuery, range }: Props) {
const [isQueryRunnable, setIsQueryRunnable] = useState(true);
const db = datasource.getDB();
const { loading, error } = useAsync(async () => {
return () => {
if (datasource.getDB(datasource.id).init !== undefined) {
datasource.getDB(datasource.id).init!();
}
};
}, [datasource]);
const queryWithDefaults = applyQueryDefaults(query);
const [queryRowFilter, setQueryRowFilter] = useState<QueryRowFilter>({
filter: !!queryWithDefaults.sql?.whereString,
group: !!queryWithDefaults.sql?.groupBy?.[0]?.property.name,
order: !!queryWithDefaults.sql?.orderBy?.property.name,
preview: true,
});
const [queryToValidate, setQueryToValidate] = useState(queryWithDefaults);
useEffect(() => {
return () => {
if (datasource.getDB(datasource.id).dispose !== undefined) {
datasource.getDB(datasource.id).dispose!();
}
};
}, [datasource]);
const processQuery = useCallback(
(q: SQLQuery) => {
if (isQueryValid(q) && onRunQuery) {
onRunQuery();
}
},
[onRunQuery]
);
const onQueryChange = (q: SQLQuery, process = true) => {
setQueryToValidate(q);
onChange(q);
if (haveColumns(q.sql?.columns) && q.sql?.columns.some((c) => c.name) && !queryRowFilter.group) {
setQueryRowFilter({ ...queryRowFilter, group: true });
}
if (process) {
processQuery(q);
}
};
const onQueryHeaderChange = (q: SQLQuery) => {
setQueryToValidate(q);
onChange(q);
};
if (loading || error) {
return null;
}
return (
<>
<QueryHeader
db={db}
onChange={onQueryHeaderChange}
onRunQuery={onRunQuery}
onQueryRowChange={setQueryRowFilter}
queryRowFilter={queryRowFilter}
query={queryWithDefaults}
isQueryRunnable={isQueryRunnable}
/>
<Space v={0.5} />
{queryWithDefaults.editorMode !== EditorMode.Code && (
<VisualEditor
db={db}
query={queryWithDefaults}
onChange={(q: SQLQuery) => onQueryChange(q, false)}
queryRowFilter={queryRowFilter}
onValidate={setIsQueryRunnable}
range={range}
/>
)}
{queryWithDefaults.editorMode === EditorMode.Code && (
<RawEditor
db={db}
query={queryWithDefaults}
queryToValidate={queryToValidate}
onChange={onQueryChange}
onRunQuery={onRunQuery}
onValidate={setIsQueryRunnable}
range={range}
/>
)}
</>
);
}
const isQueryValid = (q: SQLQuery) => {
return Boolean(q.rawSql);
};
@@ -1,257 +0,0 @@
import React, { useCallback, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { SelectableValue } from '@grafana/data';
import {
Button,
EditorField,
EditorHeader,
EditorRow,
FlexItem,
InlineField,
InlineSelect,
InlineSwitch,
RadioButtonGroup,
Select,
Space,
Tooltip,
} from '@grafana/ui';
import { QueryWithDefaults } from '../defaults';
import { SQLQuery, QueryFormat, QueryRowFilter, QUERY_FORMAT_OPTIONS, DB, EditorMode } from '../types';
import { defaultToRawSql } from '../utils/sql.utils';
import { ConfirmModal } from './ConfirmModal';
import { DatasetSelector } from './DatasetSelector';
import { ErrorBoundary } from './ErrorBoundary';
import { TableSelector } from './TableSelector';
interface QueryHeaderProps {
db: DB;
query: QueryWithDefaults;
onChange: (query: SQLQuery) => void;
onRunQuery: () => void;
onQueryRowChange: (queryRowFilter: QueryRowFilter) => void;
queryRowFilter: QueryRowFilter;
isQueryRunnable: boolean;
}
const editorModes = [
{ label: 'Builder', value: EditorMode.Builder },
{ label: 'Code', value: EditorMode.Code },
];
export function QueryHeader({
db,
query,
queryRowFilter,
onChange,
onRunQuery,
onQueryRowChange,
isQueryRunnable,
}: QueryHeaderProps) {
const { editorMode } = query;
const [_, copyToClipboard] = useCopyToClipboard();
const [showConfirm, setShowConfirm] = useState(false);
const toRawSql = db.toRawSql || defaultToRawSql;
const onEditorModeChange = useCallback(
(newEditorMode: EditorMode) => {
if (editorMode === EditorMode.Code) {
setShowConfirm(true);
return;
}
onChange({ ...query, editorMode: newEditorMode });
},
[editorMode, onChange, query]
);
const onFormatChange = (e: SelectableValue) => {
const next = { ...query, format: e.value !== undefined ? e.value : QueryFormat.Table };
onChange(next);
};
const onDatasetChange = (e: SelectableValue) => {
if (e.value === query.dataset) {
return;
}
const next = {
...query,
dataset: e.value,
table: undefined,
sql: undefined,
rawSql: '',
};
onChange(next);
};
const onTableChange = (e: SelectableValue) => {
if (e.value === query.table) {
return;
}
const next: SQLQuery = {
...query,
table: e.value,
sql: undefined,
rawSql: '',
};
onChange(next);
};
return (
<>
<EditorHeader>
{/* Backward compatibility check. Inline select uses SelectContainer that was added in 8.3 */}
<ErrorBoundary
fallBackComponent={
<InlineField label="Format" labelWidth={15}>
<Select
placeholder="Select format"
value={query.format}
onChange={onFormatChange}
options={QUERY_FORMAT_OPTIONS}
/>
</InlineField>
}
>
<InlineSelect
label="Format"
value={query.format}
placeholder="Select format"
menuShouldPortal
onChange={onFormatChange}
options={QUERY_FORMAT_OPTIONS}
/>
</ErrorBoundary>
{editorMode === EditorMode.Builder && (
<>
<InlineSwitch
id="sql-filter"
label="Filter"
transparent={true}
showLabel={true}
value={queryRowFilter.filter}
onChange={(ev) =>
ev.target instanceof HTMLInputElement &&
onQueryRowChange({ ...queryRowFilter, filter: ev.target.checked })
}
/>
<InlineSwitch
id="sql-group"
label="Group"
transparent={true}
showLabel={true}
value={queryRowFilter.group}
onChange={(ev) =>
ev.target instanceof HTMLInputElement &&
onQueryRowChange({ ...queryRowFilter, group: ev.target.checked })
}
/>
<InlineSwitch
id="sql-order"
label="Order"
transparent={true}
showLabel={true}
value={queryRowFilter.order}
onChange={(ev) =>
ev.target instanceof HTMLInputElement &&
onQueryRowChange({ ...queryRowFilter, order: ev.target.checked })
}
/>
<InlineSwitch
id="sql-preview"
label="Preview"
transparent={true}
showLabel={true}
value={queryRowFilter.preview}
onChange={(ev) =>
ev.target instanceof HTMLInputElement &&
onQueryRowChange({ ...queryRowFilter, preview: ev.target.checked })
}
/>
</>
)}
<FlexItem grow={1} />
{isQueryRunnable ? (
<Button icon="play" variant="primary" size="sm" onClick={() => onRunQuery()}>
Run query
</Button>
) : (
<Tooltip
theme="error"
content={
<>
Your query is invalid. Check below for details. <br />
However, you can still run this query.
</>
}
placement="top"
>
<Button icon="exclamation-triangle" variant="secondary" size="sm" onClick={() => onRunQuery()}>
Run query
</Button>
</Tooltip>
)}
<RadioButtonGroup options={editorModes} size="sm" value={editorMode} onChange={onEditorModeChange} />
<ConfirmModal
isOpen={showConfirm}
onCopy={() => {
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)}
/>
</EditorHeader>
{editorMode === EditorMode.Builder && (
<>
<Space v={0.5} />
<EditorRow>
<EditorField label="Dataset" width={25}>
<DatasetSelector
db={db}
value={query.dataset === undefined ? null : query.dataset}
onChange={onDatasetChange}
/>
</EditorField>
<EditorField label="Table" width={25}>
<TableSelector
db={db}
query={query}
value={query.table === undefined ? null : query.table}
onChange={onTableChange}
applyDefault
/>
</EditorField>
</EditorRow>
</>
)}
</>
);
}
@@ -1,39 +0,0 @@
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<TableSelectorProps> = ({ 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 (
<Select
className={className}
disabled={state.loading}
aria-label="Table selector"
value={value}
options={state.value}
onChange={onChange}
isLoading={state.loading}
menuShouldPortal={true}
placeholder={state.loading ? 'Loading tables' : 'Select table'}
/>
);
};
@@ -1,75 +0,0 @@
import React from 'react';
import { FieldSet, InlineField } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { SQLConnectionLimits } from '../../types';
interface Props<T> {
onPropertyChanged: (property: keyof T, value?: number) => void;
labelWidth: number;
jsonData: SQLConnectionLimits;
}
export const ConnectionLimits = <T extends SQLConnectionLimits>(props: Props<T>) => {
const { onPropertyChanged, labelWidth, jsonData } = props;
const onJSONDataNumberChanged = (property: keyof SQLConnectionLimits) => {
return (number?: number) => {
if (onPropertyChanged) {
onPropertyChanged(property, number);
}
};
};
return (
<FieldSet label="Connection limits">
<InlineField
tooltip={
<span>
The maximum number of open connections to the database.If <i>Max idle connections</i> is greater than 0 and
the <i>Max open connections</i> is less than <i>Max idle connections</i>, then
<i>Max idle connections</i> will be reduced to match the <i>Max open connections</i> limit. If set to 0,
there is no limit on the number of open connections.
</span>
}
labelWidth={labelWidth}
label="Max open"
>
<NumberInput
placeholder="unlimited"
value={jsonData.maxOpenConns}
onChange={onJSONDataNumberChanged('maxOpenConns')}
></NumberInput>
</InlineField>
<InlineField
tooltip={
<span>
The maximum number of connections in the idle connection pool.If <i>Max open connections</i> is greater than
0 but less than the <i>Max idle connections</i>, then the <i>Max idle connections</i> will be reduced to
match the <i>Max open connections</i> limit. If set to 0, no idle connections are retained.
</span>
}
labelWidth={labelWidth}
label="Max idle"
>
<NumberInput
placeholder="2"
value={jsonData.maxIdleConns}
onChange={onJSONDataNumberChanged('maxIdleConns')}
></NumberInput>
</InlineField>
<InlineField
tooltip="The maximum amount of time in seconds a connection may be reused. If set to 0, connections are reused forever."
labelWidth={labelWidth}
label="Max lifetime"
>
<NumberInput
placeholder="14400"
value={jsonData.connMaxLifetime}
onChange={onJSONDataNumberChanged('connMaxLifetime')}
></NumberInput>
</InlineField>
</FieldSet>
);
};
@@ -1,77 +0,0 @@
import React from 'react';
import {
DataSourceJsonData,
DataSourcePluginOptionsEditorProps,
KeyValue,
onUpdateDatasourceSecureJsonDataOption,
updateDatasourcePluginResetOption,
} from '@grafana/data';
import { InlineField, SecretTextArea } from '@grafana/ui';
export interface Props<T, S> {
editorProps: DataSourcePluginOptionsEditorProps<T, S>;
showCACert?: boolean;
secureJsonFields?: KeyValue<Boolean>;
labelWidth?: number;
}
export const TLSSecretsConfig = <T extends DataSourceJsonData, S = {}>(props: Props<T, S>) => {
const { labelWidth, editorProps, showCACert } = props;
const { secureJsonFields } = editorProps.options;
return (
<>
<InlineField
tooltip={<span>To authenticate with an TLS/SSL client certificate, provide the client certificate here.</span>}
labelWidth={labelWidth}
label="TLS/SSL Client Certificate"
>
<SecretTextArea
placeholder="Begins with -----BEGIN CERTIFICATE-----"
cols={45}
rows={7}
isConfigured={secureJsonFields && secureJsonFields.tlsClientCert}
onChange={onUpdateDatasourceSecureJsonDataOption(editorProps, 'tlsClientCert')}
onReset={() => {
updateDatasourcePluginResetOption(editorProps, 'tlsClientCert');
}}
></SecretTextArea>
</InlineField>
{showCACert ? (
<InlineField
tooltip={<span>If the selected TLS/SSL mode requires a server root certificate, provide it here.</span>}
labelWidth={labelWidth}
label="TLS/SSL Root Certificate"
>
<SecretTextArea
placeholder="Begins with -----BEGIN CERTIFICATE-----"
cols={45}
rows={7}
isConfigured={secureJsonFields && secureJsonFields.tlsCACert}
onChange={onUpdateDatasourceSecureJsonDataOption(editorProps, 'tlsCACert')}
onReset={() => {
updateDatasourcePluginResetOption(editorProps, 'tlsCACert');
}}
></SecretTextArea>
</InlineField>
) : null}
<InlineField
tooltip={<span>To authenticate with a client TLS/SSL certificate, provide the key here.</span>}
labelWidth={labelWidth}
label="TLS/SSL Client Key"
>
<SecretTextArea
placeholder="Begins with -----BEGIN RSA PRIVATE KEY-----"
cols={45}
rows={7}
isConfigured={secureJsonFields && secureJsonFields.tlsClientKey}
onChange={onUpdateDatasourceSecureJsonDataOption(editorProps, 'tlsClientKey')}
onReset={() => {
updateDatasourcePluginResetOption(editorProps, 'tlsClientKey');
}}
></SecretTextArea>
</InlineField>
</>
);
};
@@ -1,2 +0,0 @@
export * from './query-editor-raw';
export * from './visual-query-builder';
@@ -1,47 +0,0 @@
import React, { useCallback, useEffect, useRef } from 'react';
import { LanguageCompletionProvider, SQLQuery } from '../../types';
import { formatSQL } from '../../utils/formatSQL';
import { SQLEditor } from './SQLEditor';
type Props = {
query: SQLQuery;
onChange: (value: SQLQuery, processQuery: boolean) => void;
children?: (props: { formatQuery: () => void }) => React.ReactNode;
width?: number;
height?: number;
completionProvider: LanguageCompletionProvider;
};
export function QueryEditorRaw({ children, onChange, query, width, height, completionProvider }: Props) {
// We need to pass query via ref to SQLEditor as onChange is executed via monacoEditor.onDidChangeModelContent callback, not onChange property
const queryRef = useRef<SQLQuery>(query);
useEffect(() => {
queryRef.current = query;
}, [query]);
const onRawQueryChange = useCallback(
(rawSql: string, processQuery: boolean) => {
const newQuery = {
...queryRef.current,
rawQuery: true,
rawSql,
};
onChange(newQuery, processQuery);
},
[onChange]
);
return (
<SQLEditor
width={width}
height={height}
query={query.rawSql!}
onChange={onRawQueryChange}
language={{ id: 'sql', completionProvider, formatter: formatSQL }}
>
{children}
</SQLEditor>
);
}
@@ -1,91 +0,0 @@
import { css } from '@emotion/css';
import React, { useMemo, useState } from 'react';
import { HorizontalGroup, Icon, IconButton, Tooltip, useTheme2 } from '@grafana/ui';
import { QueryValidator, QueryValidatorProps } from './QueryValidator';
interface QueryToolboxProps extends Omit<QueryValidatorProps, 'onValidate'> {
showTools?: boolean;
isExpanded?: boolean;
onFormatCode?: () => void;
onExpand?: (expand: boolean) => void;
onValidate?: (isValid: boolean) => void;
}
export function QueryToolbox({ showTools, onFormatCode, onExpand, isExpanded, ...validatorProps }: QueryToolboxProps) {
const theme = useTheme2();
const [validationResult, setValidationResult] = useState<boolean>();
const styles = useMemo(() => {
return {
container: css`
border: 1px solid ${theme.colors.border.medium};
border-top: none;
padding: ${theme.spacing(0.5, 0.5, 0.5, 0.5)};
display: flex;
flex-grow: 1;
justify-content: space-between;
font-size: ${theme.typography.bodySmall.fontSize};
`,
error: css`
color: ${theme.colors.error.text};
font-size: ${theme.typography.bodySmall.fontSize};
font-family: ${theme.typography.fontFamilyMonospace};
`,
valid: css`
color: ${theme.colors.success.text};
`,
info: css`
color: ${theme.colors.text.secondary};
`,
hint: css`
color: ${theme.colors.text.disabled};
white-space: nowrap;
cursor: help;
`,
};
}, [theme]);
let style = {};
if (!showTools && validationResult === undefined) {
style = { height: 0, padding: 0, visibility: 'hidden' };
}
return (
<div className={styles.container} style={style}>
<div>
{validatorProps.onValidate && (
<QueryValidator
{...validatorProps}
onValidate={(result: boolean) => {
setValidationResult(result);
validatorProps.onValidate!(result);
}}
/>
)}
</div>
{showTools && (
<div>
<HorizontalGroup spacing="sm">
{onFormatCode && (
<IconButton onClick={onFormatCode} name="brackets-curly" size="xs" tooltip="Format query" />
)}
{onExpand && (
<IconButton
onClick={() => onExpand(!isExpanded)}
name={isExpanded ? 'angle-up' : 'angle-down'}
size="xs"
tooltip={isExpanded ? 'Collapse editor' : 'Expand editor'}
/>
)}
<Tooltip content="Hit CTRL/CMD+Return to run query">
<Icon className={styles.hint} name="keyboard" />
</Tooltip>
</HorizontalGroup>
</div>
)}
</div>
);
}
@@ -1,110 +0,0 @@
import { css } from '@emotion/css';
import React, { useState, useMemo, useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import useDebounce from 'react-use/lib/useDebounce';
import { formattedValueToString, getValueFormat, TimeRange } from '@grafana/data';
import { Icon, Spinner, useTheme2 } from '@grafana/ui';
import { DB, SQLQuery, ValidationResults } from '../../types';
export interface QueryValidatorProps {
db: DB;
query: SQLQuery;
range?: TimeRange;
onValidate: (isValid: boolean) => void;
}
export function QueryValidator({ db, query, onValidate, range }: QueryValidatorProps) {
const [validationResult, setValidationResult] = useState<ValidationResults | null>();
const theme = useTheme2();
const valueFormatter = useMemo(() => getValueFormat('bytes'), []);
const styles = useMemo(() => {
return {
error: css`
color: ${theme.colors.error.text};
font-size: ${theme.typography.bodySmall.fontSize};
font-family: ${theme.typography.fontFamilyMonospace};
`,
valid: css`
color: ${theme.colors.success.text};
`,
info: css`
color: ${theme.colors.text.secondary};
`,
};
}, [theme]);
const [state, validateQuery] = useAsyncFn(
async (q: SQLQuery) => {
if (q.rawSql?.trim() === '') {
return null;
}
return await db.validateQuery(q, range);
},
[db]
);
const [,] = useDebounce(
async () => {
const result = await validateQuery(query);
if (result) {
setValidationResult(result);
}
return null;
},
1000,
[query, validateQuery]
);
useEffect(() => {
if (validationResult?.isError) {
onValidate(false);
}
if (validationResult?.isValid) {
onValidate(true);
}
}, [validationResult, onValidate]);
if (!state.value && !state.loading) {
return null;
}
const error = state.value?.error ? processErrorMessage(state.value.error) : '';
return (
<>
{state.loading && (
<div className={styles.info}>
<Spinner inline={true} size={12} /> Validating query...
</div>
)}
{!state.loading && state.value && (
<>
<>
{state.value.isValid && state.value.statistics && (
<div className={styles.valid}>
<Icon name="check" /> This query will process{' '}
<strong>{formattedValueToString(valueFormatter(state.value.statistics.TotalBytesProcessed))}</strong>{' '}
when run.
</div>
)}
</>
<>{state.value.isError && <div className={styles.error}>{error}</div>}</>
</>
)}
</>
);
}
function processErrorMessage(error: string) {
const splat = error.split(':');
if (splat.length > 2) {
return splat.slice(2).join(':');
}
return error;
}
@@ -1,135 +0,0 @@
## SQLEditor
### Core concepts
- `SuggestionKind` - a descriptive string representing a type of a suggestion, i.e. `SelectKeyword`, `Tables`, `LogicalOperators` etc.
- `LinkedToken` - linked list element representing each individual token with a query. Allows traversing the query back and forth. Used by `StatementPositionResolver`(see below)
- `StatementPosition` - a desctiptive string representing cursor/token position within the query. Each statement position is defined together with `StatementPositionResolver` that, given some position context, returns a boolean value indicating whether or not we are in a given `StatementPosition` position.
```ts
export type StatementPositionResolver = (
currentToken: LinkedToken | null,
previousKeyword: LinkedToken | null,
previousNonWhiteSpace: LinkedToken | null,
previousIsSlash: Boolean // To be removed as it's CloudWatch specific
) => Boolean;
```
- `SuggestionKind` and `StatementPosition` are glued together via suggestions kind registry (language specific!). This registry contains items of `SuggestionKindRegistyItem` type of the following interface:
```ts
export interface SuggestionKindRegistyItem extends RegistryItem {
id: StatementPosition;
kind: SuggestionKind[];
}
```
This item defines what kinds of suggestions should be provided in a given statement position
- Registries. There are couple of different registries used that drive the autocomplete mechanism.
- **Language specific**: functions registry, operators registry, suggestion kinds registries and statement position resolvers registires. Those registires contain SQL defaults as well as allow extension per language type.
- **Instance specific**: Registry of `SuggestionsRegistyItem` items that glue particular `SuggestionKind` with an async function that provides completion items for it.
```ts
export interface SuggestionsRegistyItem extends RegistryItem {
id: SuggestionKind;
suggestions: (position: PositionContext, m: typeof monacoTypes) => Promise<CustomSuggestion[]>;
}
```
Think about instance-specific registry as having i.e. mixed data source with multiple query editors for the same type of data source and you wish to provide only table suggestions that are valid for particular query row.
### SQLEditor component
Goals
- [ ] Allow providing suggestions for standard-ish SQL syntax (THIS PR)
- [ ] Allow providing custom SQL dialects and suggestions for them (TODO - CloudWatch implementation sets a good base for how to provide custom dialect definition)
`SQLEditor` component builds on top of `CodeEditor` component, but we may want to base it on `ReactMonacoEditor` component instead to be less prone to `CodeEditor` API changes and have full controll over the Monaco API. For now the `CodeEditor` is good enough for a simplification.
`SQLEditor` API:
```ts
interface SQLEditorProps {
query: string;
onChange: (q: string) => void;
language?: LanguageDefinition;
}
```
The important part is the `LanguageDefinition` interface which provides way to customize the completion both on a language and instance level:
```ts
interface LanguageDefinition extends monacoTypes.languages.ILanguageExtensionPoint {
// TODO: Will allow providing a custom language definition.
loadLanguage?: (module: any) => Promise<void>;
// Provides API for customizing the autocomplete
completionProvider?: (m: Monaco) => SQLCompletionItemProvider;
}
```
The `completionProvider` function is the core of the autocomplete customization. `SQLEditor` comes with standard SQL completion items, but this function allows:
- providing dynamic suggestions: tables, columns
- providing custom `StatementPositionResolvers` that are specific for a given dialect or not implemented yet for standard SQL
- providing custom `SuggestionKind` and resolvers for this kind of suggestions.
```ts
export interface SQLCompletionItemProvider
extends Omit<monacoTypes.languages.CompletionItemProvider, 'provideCompletionItems'> {
/**
* Allows dialect specific functions to be added to the completion list.
* @alpha
*/
supportedFunctions?: () => Array<{
id: string;
name: string;
}>;
/**
* Allows dialect specific operators to be added to the completion list.
* @alpha
*/
supportedOperators?: () => Array<{
id: string;
operator: string;
type: OperatorType;
}>;
/**
* Allows adding macros that are available in the dialect datasource.
* @alpha
*/
supportedMacros?: () => Array<{
id: string;
name: string;
type: MacroType;
args: Array<string>;
}>;
/**
* Allows custom suggestion kinds to be defined and correlate them with <Custom>StatementPosition.
* @alpha
*/
customSuggestionKinds?: () => CustomSuggestionKind[];
/**
* Allows custom statement placement definition.
* @alpha
*/
customStatementPlacement?: () => CustomStatementPlacement[];
/**
* Allows providing a custom function for resolving db tables.
* It's up to the consumer to decide whether the columns are resolved via API calls or preloaded in the query editor(i.e. full db schema is preloades loaded).
* @alpha
*/
tables?: {
resolve: () => Promise<TableDefinition[]>;
// Allows providing a custom function for calculating the table name from the query. If not specified a default implemnentation is used. I.e. BigQuery requires the table name to be fully qualified name: <project>.<dataset>.<table>
parseName?: (t: LinkedToken) => string;
};
/**
* Allows providing a custom function for resolving table.
* It's up to the consumer to decide whether the columns are resolved via API calls or preloaded in the query editor(i.e. full db schema is preloades loaded).
* @alpha
*/
columns?: {
resolve: (table: string) => Promise<ColumnDefinition[]>;
};
}
```
@@ -1,121 +0,0 @@
import { css } from '@emotion/css';
import React, { useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Modal, useStyles2, useTheme2 } from '@grafana/ui';
import { SQLQuery, QueryEditorProps } from '../../types';
import { QueryEditorRaw } from './QueryEditorRaw';
import { QueryToolbox } from './QueryToolbox';
interface RawEditorProps extends Omit<QueryEditorProps, 'onChange'> {
onRunQuery: () => void;
onChange: (q: SQLQuery, processQuery: boolean) => void;
onValidate: (isValid: boolean) => void;
queryToValidate: SQLQuery;
}
export function RawEditor({ db, query, onChange, onRunQuery, onValidate, queryToValidate, range }: RawEditorProps) {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const [isExpanded, setIsExpanded] = useState(false);
const [toolboxRef, toolboxMeasure] = useMeasure<HTMLDivElement>();
const [editorRef, editorMeasure] = useMeasure<HTMLDivElement>();
const completionProvider = useMemo(() => db.getSqlCompletionProvider(), [db]);
const renderQueryEditor = (width?: number, height?: number) => {
return (
<QueryEditorRaw
completionProvider={completionProvider}
query={query}
width={width}
height={height ? height - toolboxMeasure.height : undefined}
onChange={onChange}
>
{({ formatQuery }) => {
return (
<div ref={toolboxRef}>
<QueryToolbox
db={db}
query={queryToValidate}
onValidate={onValidate}
onFormatCode={formatQuery}
showTools
range={range}
onExpand={setIsExpanded}
isExpanded={isExpanded}
/>
</div>
);
}}
</QueryEditorRaw>
);
};
const renderEditor = (standalone = false) => {
return standalone ? (
<AutoSizer>
{({ width, height }) => {
return renderQueryEditor(width, height);
}}
</AutoSizer>
) : (
<div ref={editorRef}>{renderQueryEditor()}</div>
);
};
const renderPlaceholder = () => {
return (
<div
style={{
width: editorMeasure.width,
height: editorMeasure.height,
background: theme.colors.background.primary,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Editing in expanded code editor
</div>
);
};
return (
<>
{isExpanded ? renderPlaceholder() : renderEditor()}
{isExpanded && (
<Modal
title={`Query ${query.refId}`}
closeOnBackdropClick={false}
closeOnEscape={false}
className={styles.modal}
contentClassName={styles.modalContent}
isOpen={isExpanded}
onDismiss={() => {
setIsExpanded(false);
}}
>
{renderEditor(true)}
</Modal>
)}
</>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
modal: css`
width: 95vw;
height: 95vh;
`,
modalContent: css`
height: 100%;
padding-top: 0;
`,
};
}
@@ -1,405 +0,0 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { v4 } from 'uuid';
import { Registry } from '@grafana/data';
import { CodeEditor, Monaco, monacoTypes } from '@grafana/ui';
import standardSQLLanguageDefinition from '../../standardSql/definition';
import { getStandardSuggestions } from '../../standardSql/getStandardSuggestions';
import { getStatementPosition } from '../../standardSql/getStatementPosition';
import {
initFunctionsRegistry,
initMacrosRegistry,
initOperatorsRegistry,
initStandardSuggestions,
} from '../../standardSql/standardSuggestionsRegistry';
import { initStatementPositionResolvers } from '../../standardSql/statementPositionResolversRegistry';
import { initSuggestionsKindRegistry, SuggestionKindRegistryItem } from '../../standardSql/suggestionsKindRegistry';
import {
FunctionsRegistryItem,
MacrosRegistryItem,
OperatorsRegistryItem,
SQLMonarchLanguage,
StatementPositionResolversRegistryItem,
SuggestionsRegistryItem,
} from '../../standardSql/types';
import {
CompletionItemKind,
CompletionItemPriority,
CustomSuggestion,
PositionContext,
SQLCompletionItemProvider,
StatementPosition,
SuggestionKind,
} from '../../types';
import { LinkedToken } from '../../utils/LinkedToken';
import { TRIGGER_SUGGEST } from '../../utils/commands';
import { sqlEditorLog } from '../../utils/debugger';
import { getSuggestionKinds } from '../../utils/getSuggestionKind';
import { linkedTokenBuilder } from '../../utils/linkedTokenBuilder';
import { getTableToken } from '../../utils/tokenUtils';
const STANDARD_SQL_LANGUAGE = 'sql';
export interface LanguageDefinition extends monacoTypes.languages.ILanguageExtensionPoint {
loader?: (module: any) => Promise<{
language: SQLMonarchLanguage;
conf: monacoTypes.languages.LanguageConfiguration;
}>;
// Provides API for customizing the autocomplete
completionProvider?: (m: Monaco) => SQLCompletionItemProvider;
// Function that returns a formatted query
formatter?: (q: string) => string;
}
interface SQLEditorProps {
query: string;
/**
* Use for inspecting the query as it changes. I.e. for validation.
*/
onChange?: (q: string, processQuery: boolean) => void;
language?: LanguageDefinition;
children?: (props: { formatQuery: () => void }) => React.ReactNode;
width?: number;
height?: number;
}
const defaultTableNameParser = (t: LinkedToken) => t.value;
interface LanguageRegistries {
functions: Registry<FunctionsRegistryItem>;
operators: Registry<OperatorsRegistryItem>;
suggestionKinds: Registry<SuggestionKindRegistryItem>;
positionResolvers: Registry<StatementPositionResolversRegistryItem>;
macros: Registry<MacrosRegistryItem>;
}
const LANGUAGES_CACHE = new Map<string, LanguageRegistries>();
const INSTANCE_CACHE = new Map<string, Registry<SuggestionsRegistryItem>>();
export const SQLEditor: React.FC<SQLEditorProps> = ({
children,
onChange,
query,
language = { id: STANDARD_SQL_LANGUAGE },
width,
height,
}) => {
const monacoRef = useRef<monacoTypes.editor.IStandaloneCodeEditor>(null);
const langUid = useRef<string>();
// create unique language id for each SQLEditor instance
const id = useMemo(() => {
const uid = v4();
const id = `${language.id}-${uid}`;
langUid.current = id;
return id;
}, [language.id]);
useEffect(() => {
return () => {
INSTANCE_CACHE.delete(langUid.current!);
sqlEditorLog(`Removing instance cache ${langUid.current}`, false, INSTANCE_CACHE);
};
}, []);
const formatQuery = useCallback(() => {
if (monacoRef.current) {
monacoRef.current.getAction('editor.action.formatDocument').run();
}
}, []);
return (
<div style={{ width }}>
<CodeEditor
height={height || '240px'}
// -2px to compensate for borders width
width={width ? `${width - 2}px` : undefined}
language={id}
value={query}
onBlur={(v) => onChange && onChange(v, false)}
showMiniMap={false}
showLineNumbers={true}
// Using onEditorDidMount instead of onBeforeEditorMount to support Grafana < 8.2.x
onEditorDidMount={(editor, m) => {
// TODO - says its read only but this worked in experimental
// monacoRef.current = editor;
editor.onDidChangeModelContent((e) => {
const text = editor.getValue();
if (onChange) {
onChange(text, false);
}
});
editor.addCommand(m.KeyMod.CtrlCmd | m.KeyCode.Enter, () => {
const text = editor.getValue();
if (onChange) {
onChange(text, true);
}
});
registerLanguageAndSuggestions(m, language, id);
}}
/>
{children && children({ formatQuery })}
</div>
);
};
// There's three ways to define Monaco language:
// 1. Leave language.id empty or set it to 'sql'. This will load a standard sql language definition, including syntax highlighting and tokenization for
// common Grafana entities such as macros and template variables
// 2. Provide a custom language and load it via the async LanguageDefinition.loader callback
// 3. Specify a language.id that exists in the Monaco language registry. See available languages here: https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages
// If a custom language is specified, its LanguageDefinition will be merged with the LanguageDefinition for standard SQL. This allows the consumer to only
// override parts of the LanguageDefinition, such as for example the completion item provider.
const resolveLanguage = (monaco: Monaco, languageDefinitionProp: LanguageDefinition): LanguageDefinition => {
if (languageDefinitionProp?.id !== STANDARD_SQL_LANGUAGE && !languageDefinitionProp.loader) {
sqlEditorLog(`Loading language '${languageDefinitionProp?.id}' from Monaco registry`, false);
const allLangs = monaco.languages.getLanguages();
const custom = allLangs.find(({ id }) => id === languageDefinitionProp?.id);
if (!custom) {
throw Error(`Unknown Monaco language ${languageDefinitionProp?.id}`);
}
return custom;
}
return {
...standardSQLLanguageDefinition,
...languageDefinitionProp,
};
};
export const registerLanguageAndSuggestions = async (monaco: Monaco, l: LanguageDefinition, lid: string) => {
const languageDefinition = resolveLanguage(monaco, l);
const { language, conf } = await languageDefinition.loader!(monaco);
monaco.languages.register({ id: lid });
monaco.languages.setMonarchTokensProvider(lid, { ...language });
monaco.languages.setLanguageConfiguration(lid, { ...conf });
if (languageDefinition.formatter) {
monaco.languages.registerDocumentFormattingEditProvider(lid, {
provideDocumentFormattingEdits: (model) => {
var formatted = l.formatter!(model.getValue());
return [
{
range: model.getFullModelRange(),
text: formatted,
},
];
},
});
}
if (languageDefinition.completionProvider) {
const customProvider = l.completionProvider!(monaco);
extendStandardRegistries(l.id, lid, customProvider);
const languageSuggestionsRegistries = LANGUAGES_CACHE.get(l.id)!;
const instanceSuggestionsRegistry = INSTANCE_CACHE.get(lid)!;
const completionProvider: monacoTypes.languages.CompletionItemProvider['provideCompletionItems'] = async (
model,
position,
context,
token
) => {
const currentToken = linkedTokenBuilder(monaco, model, position, 'sql');
const statementPosition = getStatementPosition(currentToken, languageSuggestionsRegistries.positionResolvers);
const kind = getSuggestionKinds(statementPosition, languageSuggestionsRegistries.suggestionKinds);
sqlEditorLog('Statement position', false, statementPosition);
sqlEditorLog('Suggestion kinds', false, kind);
const ctx: PositionContext = {
position,
currentToken,
statementPosition,
kind,
range: monaco.Range.fromPositions(position),
};
// // Completely custom suggestions - hope this won't we needed
// let ci;
// if (customProvider.provideCompletionItems) {
// ci = customProvider.provideCompletionItems(model, position, context, token, ctx);
// }
const stdSuggestions = await getStandardSuggestions(monaco, currentToken, kind, ctx, instanceSuggestionsRegistry);
return {
// ...ci,
suggestions: stdSuggestions,
};
};
monaco.languages.registerCompletionItemProvider(lid, {
...customProvider,
provideCompletionItems: completionProvider,
});
}
};
function extendStandardRegistries(id: string, lid: string, customProvider: SQLCompletionItemProvider) {
if (!LANGUAGES_CACHE.has(id)) {
initializeLanguageRegistries(id);
}
const languageRegistries = LANGUAGES_CACHE.get(id)!;
if (!INSTANCE_CACHE.has(lid)) {
INSTANCE_CACHE.set(
lid,
new Registry(
initStandardSuggestions(languageRegistries.functions, languageRegistries.operators, languageRegistries.macros)
)
);
}
const instanceSuggestionsRegistry = INSTANCE_CACHE.get(lid)!;
if (customProvider.supportedFunctions) {
for (const func of customProvider.supportedFunctions()) {
const exists = languageRegistries.functions.getIfExists(func.id);
if (!exists) {
languageRegistries.functions.register(func);
}
}
}
if (customProvider.supportedOperators) {
for (const op of customProvider.supportedOperators()) {
const exists = languageRegistries.operators.getIfExists(op.id);
if (!exists) {
languageRegistries.operators.register({ ...op, name: op.id });
}
}
}
if (customProvider.supportedMacros) {
for (const macro of customProvider.supportedMacros()) {
const exists = languageRegistries.macros.getIfExists(macro.id);
if (!exists) {
languageRegistries.macros.register({ ...macro, name: macro.id });
}
}
}
if (customProvider.customStatementPlacement) {
for (const placement of customProvider.customStatementPlacement()) {
const exists = languageRegistries.positionResolvers.getIfExists(placement.id);
if (!exists) {
languageRegistries.positionResolvers.register({
...placement,
id: placement.id as StatementPosition,
name: placement.id,
});
languageRegistries.suggestionKinds.register({
id: placement.id as StatementPosition,
name: placement.id,
kind: [],
});
} else {
// Allow extension to the built-in placement resolvers
const origResolve = exists.resolve;
exists.resolve = (...args) => {
const ext = placement.resolve(...args);
if (placement.overrideDefault) {
return ext;
}
const orig = origResolve(...args);
return orig || ext;
};
}
}
}
if (customProvider.customSuggestionKinds) {
for (const kind of customProvider.customSuggestionKinds()) {
kind.applyTo?.forEach((applyTo) => {
const exists = languageRegistries.suggestionKinds.getIfExists(applyTo);
if (exists) {
// avoid duplicates
if (exists.kind.indexOf(kind.id as SuggestionKind) === -1) {
exists.kind.push(kind.id as SuggestionKind);
}
}
});
if (kind.overrideDefault) {
const stbBehaviour = instanceSuggestionsRegistry.get(kind.id);
if (stbBehaviour !== undefined) {
stbBehaviour.suggestions = kind.suggestionsResolver;
continue;
}
}
instanceSuggestionsRegistry.register({
id: kind.id as SuggestionKind,
name: kind.id,
suggestions: kind.suggestionsResolver,
});
}
}
if (customProvider.tables) {
const stbBehaviour = instanceSuggestionsRegistry.get(SuggestionKind.Tables);
const s = stbBehaviour!.suggestions;
stbBehaviour!.suggestions = async (ctx, m) => {
const o = await s(ctx, m);
const oo = (await customProvider.tables!.resolve!()).map((x) => ({
label: x.name,
insertText: x.completion ?? x.name,
command: TRIGGER_SUGGEST,
kind: CompletionItemKind.Field,
sortText: CompletionItemPriority.High,
}));
return [...o, ...oo];
};
}
if (customProvider.columns) {
const stbBehaviour = instanceSuggestionsRegistry.get(SuggestionKind.Columns);
const s = stbBehaviour!.suggestions;
stbBehaviour!.suggestions = async (ctx, m) => {
const o = await s(ctx, m);
const tableToken = getTableToken(ctx.currentToken);
let table = '';
const tableNameParser = customProvider.tables?.parseName ?? defaultTableNameParser;
if (tableToken && tableToken.value) {
table = tableNameParser(tableToken).trim();
}
let oo: CustomSuggestion[] = [];
if (table) {
const columns = await customProvider.columns?.resolve!(table);
oo = columns
? columns.map<CustomSuggestion>((x) => ({
label: x.name,
insertText: x.completion ?? x.name,
kind: CompletionItemKind.Field,
sortText: CompletionItemPriority.High,
detail: x.type,
documentation: x.description,
}))
: [];
}
return [...o, ...oo];
};
}
}
/**
* Initializes language specific registries that are treated as singletons
*/
function initializeLanguageRegistries(id: string) {
if (!LANGUAGES_CACHE.has(id)) {
LANGUAGES_CACHE.set(id, {
functions: new Registry(initFunctionsRegistry),
operators: new Registry(initOperatorsRegistry),
suggestionKinds: new Registry(initSuggestionsKindRegistry),
positionResolvers: new Registry(initStatementPositionResolvers),
macros: new Registry(initMacrosRegistry),
});
}
return LANGUAGES_CACHE.get(id)!;
}
@@ -1 +0,0 @@
export { SQLEditor, LanguageDefinition } from './SQLEditor';
@@ -1,259 +0,0 @@
import { List } from 'immutable';
import { isString } from 'lodash';
import React from 'react';
import {
BasicConfig,
Config,
JsonItem,
JsonTree,
Operator,
Settings,
SimpleField,
Utils,
ValueSource,
Widgets,
} from 'react-awesome-query-builder';
import { dateTime, toOption } from '@grafana/data';
import { Button, DateTimePicker, Input, Select } from '@grafana/ui';
const buttonLabels = {
add: 'Add',
remove: 'Remove',
};
export const emptyInitValue: JsonItem = {
id: Utils.uuid(),
type: 'group' as const,
children1: {
[Utils.uuid()]: {
type: 'rule',
properties: {
field: null,
operator: null,
value: [],
valueSrc: [],
},
},
},
};
export const emptyInitTree: JsonTree = {
id: Utils.uuid(),
type: 'group' as const,
children1: {
[Utils.uuid()]: {
type: 'rule',
properties: {
field: null,
operator: null,
value: [],
valueSrc: [],
},
},
},
};
export const widgets: Widgets = {
...BasicConfig.widgets,
text: {
...BasicConfig.widgets.text,
factory: function TextInput(props) {
return (
<Input
value={props?.value || ''}
placeholder={props?.placeholder}
onChange={(e) => props?.setValue(e.currentTarget.value)}
/>
);
},
},
number: {
...BasicConfig.widgets.number,
factory: function NumberInput(props) {
return (
<Input
value={props?.value}
placeholder={props?.placeholder}
type="number"
onChange={(e) => props?.setValue(Number.parseInt(e.currentTarget.value, 10))}
/>
);
},
},
datetime: {
...BasicConfig.widgets.datetime,
factory: function DateTimeInput(props) {
return (
<DateTimePicker
onChange={(e) => {
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 (
<Select
id={conjProps?.id}
aria-label="Conjunction"
menuShouldPortal
options={conjProps?.conjunctionOptions ? Object.keys(conjProps?.conjunctionOptions).map(toOption) : undefined}
value={conjProps?.selectedConjunction}
onChange={(val) => conjProps?.setConjunction(val.value!)}
/>
);
},
renderField: function Field(fieldProps) {
const fields = fieldProps?.config?.fields || {};
return (
<Select
id={fieldProps?.id}
width={25}
aria-label="Field"
menuShouldPortal
options={fieldProps?.items.map((f) => {
// @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 (
<Button
type="button"
title={`${buttonProps?.label} filter`}
onClick={buttonProps?.onClick}
variant="secondary"
size="md"
icon={buttonProps?.label === buttonLabels.add ? 'plus' : 'times'}
/>
);
},
renderOperator: function Operator(operatorProps) {
return (
<Select
options={operatorProps?.items.map((op) => ({ label: op.label, value: op.key }))}
aria-label="Operator"
menuShouldPortal
value={operatorProps?.selectedKey}
onChange={(val) => {
operatorProps?.setField(val.value || '');
}}
/>
);
},
};
// add IN / NOT IN operators to text to support multi-value variables
const enum Op {
IN = 'select_any_in',
NOT_IN = 'select_not_any_in',
}
// eslint-ignore
const customOperators = getCustomOperators(BasicConfig) as typeof BasicConfig.operators;
const textWidget = BasicConfig.types.text.widgets.text;
const opers = [...(textWidget.operators || []), Op.IN, Op.NOT_IN];
const customTextWidget = {
...textWidget,
operators: opers,
};
const customTypes = {
...BasicConfig.types,
text: {
...BasicConfig.types.text,
widgets: {
...BasicConfig.types.text.widgets,
text: customTextWidget,
},
},
};
export const raqbConfig: Config = {
...BasicConfig,
widgets,
settings,
operators: customOperators as typeof BasicConfig.operators,
types: customTypes,
};
export type { Config };
function getCustomOperators(config: BasicConfig) {
const { ...supportedOperators } = config.operators;
const noop = () => '';
// IN operator expects array, override IN formatter for multi-value variables
const sqlFormatInOp = supportedOperators[Op.IN].sqlFormatOp || noop;
const customSqlInFormatter = (
field: string,
op: string,
value: string | List<string>,
valueSrc: ValueSource,
valueType: string,
opDef: Operator,
operatorOptions: object,
fieldDef: SimpleField
) => {
return sqlFormatInOp(field, op, splitIfString(value), valueSrc, valueType, opDef, operatorOptions, fieldDef);
};
// NOT IN operator expects array, override NOT IN formatter for multi-value variables
const sqlFormatNotInOp = supportedOperators[Op.NOT_IN].sqlFormatOp || noop;
const customSqlNotInFormatter = (
field: string,
op: string,
value: string | List<string>,
valueSrc: ValueSource,
valueType: string,
opDef: Operator,
operatorOptions: object,
fieldDef: SimpleField
) => {
return sqlFormatNotInOp(field, op, splitIfString(value), valueSrc, valueType, opDef, operatorOptions, fieldDef);
};
const customOperators = {
...supportedOperators,
[Op.IN]: {
...supportedOperators[Op.IN],
sqlFormatOp: customSqlInFormatter,
},
[Op.NOT_IN]: {
...supportedOperators[Op.NOT_IN],
sqlFormatOp: customSqlNotInFormatter,
},
};
return customOperators;
}
// value: string | List<string> but AQB uses a different version of Immutable
// eslint-ignore
function splitIfString(value: any) {
if (isString(value)) {
return value.split(',');
}
return value;
}
@@ -1,58 +0,0 @@
import React, { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { AccessoryButton, EditorList, InputGroup, Select } from '@grafana/ui';
import { QueryEditorGroupByExpression } from '../../expressions';
import { SQLExpression } from '../../types';
import { setGroupByField } from '../../utils/sql.utils';
interface GroupByRowProps {
sql: SQLExpression;
onSqlChange: (sql: SQLExpression) => void;
columns?: Array<SelectableValue<string>>;
}
export function GroupByRow({ sql, columns, onSqlChange }: GroupByRowProps) {
const onGroupByChange = useCallback(
(item: Array<Partial<QueryEditorGroupByExpression>>) => {
// As new (empty object) items come in, we need to make sure they have the correct type
const cleaned = item.map((v) => setGroupByField(v.property?.name));
const newSql = { ...sql, groupBy: cleaned };
onSqlChange(newSql);
},
[onSqlChange, sql]
);
return (
<EditorList<QueryEditorGroupByExpression>
items={sql.groupBy!}
onChange={onGroupByChange}
renderItem={makeRenderColumn({
options: columns,
})}
/>
);
}
function makeRenderColumn({ options }: { options?: Array<SelectableValue<string>> }) {
const renderColumn = function (
item: Partial<QueryEditorGroupByExpression>,
onChangeItem: (item: QueryEditorGroupByExpression) => void,
onDeleteItem: () => void
) {
return (
<InputGroup>
<Select
value={item.property?.name ? toOption(item.property.name) : null}
aria-label="Group by"
options={options}
menuShouldPortal
onChange={({ value }) => value && onChangeItem(setGroupByField(value))}
/>
<AccessoryButton aria-label="Remove group by column" icon="times" variant="secondary" onClick={onDeleteItem} />
</InputGroup>
);
};
return renderColumn;
}
@@ -1,91 +0,0 @@
import { uniqueId } from 'lodash';
import React, { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { EditorField, Input, InputGroup, RadioButtonGroup, Select, Space } from '@grafana/ui';
import { SQLExpression } from '../../types';
import { setPropertyField } from '../../utils/sql.utils';
type OrderByRowProps = {
sql: SQLExpression;
onSqlChange: (sql: SQLExpression) => void;
columns?: Array<SelectableValue<string>>;
showOffset?: boolean;
};
const sortOrderOptions = [
{ description: 'Sort by ascending', value: 'ASC', icon: 'sort-amount-up' } as const,
{ description: 'Sort by descending', value: 'DESC', icon: 'sort-amount-down' } as const,
];
export function OrderByRow({ sql, onSqlChange, columns, showOffset }: OrderByRowProps) {
const onSortOrderChange = useCallback(
(item: 'ASC' | 'DESC') => {
const newSql: SQLExpression = { ...sql, orderByDirection: item };
onSqlChange(newSql);
},
[onSqlChange, sql]
);
const onLimitChange = useCallback(
(event: React.FormEvent<HTMLInputElement>) => {
const newSql: SQLExpression = { ...sql, limit: Number.parseInt(event.currentTarget.value, 10) };
onSqlChange(newSql);
},
[onSqlChange, sql]
);
const onOffsetChange = useCallback(
(event: React.FormEvent<HTMLInputElement>) => {
const newSql: SQLExpression = { ...sql, offset: Number.parseInt(event.currentTarget.value, 10) };
onSqlChange(newSql);
},
[onSqlChange, sql]
);
const onOrderByChange = useCallback(
(item: SelectableValue<string>) => {
const newSql: SQLExpression = { ...sql, orderBy: setPropertyField(item?.value) };
if (item === null) {
newSql.orderByDirection = undefined;
}
onSqlChange(newSql);
},
[onSqlChange, sql]
);
return (
<>
<EditorField label="Order by" width={25}>
<InputGroup>
<Select
aria-label="Order by"
options={columns}
value={sql.orderBy?.property.name ? toOption(sql.orderBy.property.name) : null}
isClearable
menuShouldPortal
onChange={onOrderByChange}
/>
<Space h={1.5} />
<RadioButtonGroup
options={sortOrderOptions}
disabled={!sql?.orderBy?.property.name}
value={sql.orderByDirection}
onChange={onSortOrderChange}
/>
</InputGroup>
</EditorField>
<EditorField label="Limit" optional width={25}>
<Input type="number" min={0} id={uniqueId('limit-')} value={sql.limit || ''} onChange={onLimitChange} />
</EditorField>
{showOffset && (
<EditorField label="Offset" optional width={25}>
<Input type="number" id={uniqueId('offset-')} value={sql.offset || ''} onChange={onOffsetChange} />
</EditorField>
)}
</>
);
}
@@ -1,46 +0,0 @@
import { css } from '@emotion/css';
import React from 'react';
import { useCopyToClipboard } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { CodeEditor, Field, IconButton, useStyles2 } from '@grafana/ui';
import { formatSQL } from '../../utils/formatSQL';
type PreviewProps = {
rawSql: string;
};
export function Preview({ rawSql }: PreviewProps) {
// TODO: use zero index to give feedback about copy success
const [_, copyToClipboard] = useCopyToClipboard();
const styles = useStyles2(getStyles);
const labelElement = (
<div className={styles.labelWrapper}>
<label className={styles.label}>Preview</label>
<IconButton tooltip="Copy to clipboard" onClick={() => copyToClipboard(rawSql)} name="copy" />
</div>
);
return (
<Field label={labelElement} className={styles.grow}>
<CodeEditor
language="sql"
height={80}
value={formatSQL(rawSql)}
monacoOptions={{ scrollbar: { vertical: 'hidden' }, scrollBeyondLastLine: false }}
readOnly={true}
showMiniMap={false}
/>
</Field>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
grow: css({ flexGrow: 1 }),
label: css({ fontSize: 12, fontWeight: theme.typography.fontWeightMedium }),
labelWrapper: css({ display: 'flex', justifyContent: 'space-between', paddingBottom: theme.spacing(0.5) }),
};
}
@@ -1,22 +0,0 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { QueryWithDefaults } from '../../defaults';
import { DB, SQLQuery } from '../../types';
import { useSqlChange } from '../../utils/useSqlChange';
import { GroupByRow } from './GroupByRow';
interface SQLGroupByRowProps {
fields: SelectableValue[];
query: QueryWithDefaults;
onQueryChange: (query: SQLQuery) => void;
db: DB;
}
export function SQLGroupByRow({ fields, query, onQueryChange, db }: SQLGroupByRowProps) {
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
return <GroupByRow columns={fields} sql={query.sql!} onSqlChange={onSqlChange} />;
}
@@ -1,42 +0,0 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { QueryWithDefaults } from '../../defaults';
import { DB, SQLQuery } from '../../types';
import { useSqlChange } from '../../utils/useSqlChange';
import { OrderByRow } from './OrderByRow';
type SQLOrderByRowProps = {
fields: SelectableValue[];
query: QueryWithDefaults;
onQueryChange: (query: SQLQuery) => void;
db: DB;
};
export function SQLOrderByRow({ fields, query, onQueryChange, db }: SQLOrderByRowProps) {
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
let columnsWithIndices: SelectableValue[] = [];
if (fields) {
const options = query.sql?.columns?.map((c, i) => {
const value = c.name ? `${c.name}(${c.parameters?.map((p) => p.name)})` : c.parameters?.map((p) => p.name);
return {
value,
label: `${i + 1} - ${value}`,
};
});
columnsWithIndices = [
{
value: '',
label: 'Selected columns',
options,
expanded: true,
},
...fields,
];
}
return <OrderByRow sql={query.sql!} onSqlChange={onSqlChange} columns={columnsWithIndices} />;
}
@@ -1,28 +0,0 @@
import React from 'react';
import { useAsync } from 'react-use';
import { SelectableValue, toOption } from '@grafana/data';
import { QueryWithDefaults } from '../../defaults';
import { DB, SQLQuery } from '../../types';
import { useSqlChange } from '../../utils/useSqlChange';
import { SelectRow } from './SelectRow';
interface SQLSelectRowProps {
fields: SelectableValue[];
query: QueryWithDefaults;
onQueryChange: (query: SQLQuery) => void;
db: DB;
}
export function SQLSelectRow({ fields, query, onQueryChange, db }: SQLSelectRowProps) {
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
const state = useAsync(async () => {
const functions = await db.functions();
return functions.map((f) => toOption(f.name));
}, [db]);
return <SelectRow columns={fields} sql={query.sql!} functions={state.value} onSqlChange={onSqlChange} />;
}
@@ -1,51 +0,0 @@
import React from 'react';
import useAsync from 'react-use/lib/useAsync';
import { SelectableValue } from '@grafana/data';
import { QueryWithDefaults } from '../../defaults';
import { DB, SQLExpression, SQLQuery, SQLSelectableValue } from '../../types';
import { useSqlChange } from '../../utils/useSqlChange';
import { Config } from './AwesomeQueryBuilder';
import { WhereRow } from './WhereRow';
interface WhereRowProps {
query: QueryWithDefaults;
fields: SelectableValue[];
onQueryChange: (query: SQLQuery) => void;
db: DB;
}
export function SQLWhereRow({ query, fields, onQueryChange, db }: WhereRowProps) {
const state = useAsync(async () => {
return mapFieldsToTypes(fields);
}, [fields]);
const { onSqlChange } = useSqlChange({ query, onQueryChange, db });
return (
<WhereRow
// TODO: fix key that's used to force clean render or SQLWhereRow - otherwise it doesn't render operators correctly
key={JSON.stringify(state.value)}
config={{ fields: state.value || {} }}
sql={query.sql!}
onSqlChange={(val: SQLExpression) => {
onSqlChange(val);
}}
/>
);
}
// needed for awesome query builder
function mapFieldsToTypes(columns: SQLSelectableValue[]) {
const fields: Config['fields'] = {};
for (const col of columns) {
fields[col.value] = {
type: col.raqbFieldType || 'text',
valueSources: ['value'],
mainWidgetProps: { customProps: { icon: col.icon } },
};
}
return fields;
}
@@ -1,141 +0,0 @@
import { css } from '@emotion/css';
import { uniqueId } from 'lodash';
import React, { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data';
import { Button, EditorField, Select, Stack, useStyles2 } from '@grafana/ui';
import { QueryEditorExpressionType, QueryEditorFunctionExpression } from '../../expressions';
import { SQLExpression } from '../../types';
import { createFunctionField } from '../../utils/sql.utils';
interface SelectRowProps {
sql: SQLExpression;
onSqlChange: (sql: SQLExpression) => void;
columns?: Array<SelectableValue<string>>;
functions?: Array<SelectableValue<string>>;
}
const asteriskValue = { label: '*', value: '*' };
export function SelectRow({ sql, columns, onSqlChange, functions }: SelectRowProps) {
const styles = useStyles2(getStyles);
const columnsWithAsterisk = [asteriskValue, ...(columns || [])];
const onColumnChange = useCallback(
(item: QueryEditorFunctionExpression, index: number) => (column: SelectableValue<string>) => {
let modifiedItem = { ...item };
if (!item.parameters?.length) {
modifiedItem.parameters = [{ type: QueryEditorExpressionType.FunctionParameter, name: column.value } as const];
} else {
modifiedItem.parameters = item.parameters.map((p) =>
p.type === QueryEditorExpressionType.FunctionParameter ? { ...p, name: column.value } : p
);
}
const newSql: SQLExpression = {
...sql,
columns: sql.columns?.map((c, i) => (i === index ? modifiedItem : c)),
};
onSqlChange(newSql);
},
[onSqlChange, sql]
);
const onAggregationChange = useCallback(
(item: QueryEditorFunctionExpression, index: number) => (aggregation: SelectableValue<string>) => {
const newItem = {
...item,
name: aggregation?.value,
};
const newSql: SQLExpression = {
...sql,
columns: sql.columns?.map((c, i) => (i === index ? newItem : c)),
};
onSqlChange(newSql);
},
[onSqlChange, sql]
);
const removeColumn = useCallback(
(index: number) => () => {
const clone = [...sql.columns!];
clone.splice(index, 1);
const newSql: SQLExpression = {
...sql,
columns: clone,
};
onSqlChange(newSql);
},
[onSqlChange, sql]
);
const addColumn = useCallback(() => {
const newSql: SQLExpression = { ...sql, columns: [...sql.columns!, createFunctionField()] };
onSqlChange(newSql);
}, [onSqlChange, sql]);
return (
<Stack gap={2} alignItems="end" wrap direction="column">
{sql.columns?.map((item, index) => (
<div key={index}>
<Stack gap={2} alignItems="end">
<EditorField label="Column" width={25}>
<Select
value={getColumnValue(item)}
options={columnsWithAsterisk}
inputId={`select-column-${index}-${uniqueId()}`}
menuShouldPortal
allowCustomValue
onChange={onColumnChange(item, index)}
/>
</EditorField>
<EditorField label="Aggregation" optional width={25}>
<Select
value={item.name ? toOption(item.name) : null}
inputId={`select-aggregation-${index}-${uniqueId()}`}
isClearable
menuShouldPortal
allowCustomValue
options={functions}
onChange={onAggregationChange(item, index)}
/>
</EditorField>
<Button
aria-label="Remove"
type="button"
icon="trash-alt"
variant="secondary"
size="md"
onClick={removeColumn(index)}
/>
</Stack>
</div>
))}
<Button
type="button"
onClick={addColumn}
variant="secondary"
size="md"
icon="plus"
aria-label="Add"
className={styles.addButton}
/>
</Stack>
);
}
const getStyles = () => {
return { addButton: css({ alignSelf: 'flex-start' }) };
};
function getColumnValue({ parameters }: QueryEditorFunctionExpression): SelectableValue<string> | null {
const column = parameters?.find((p) => p.type === QueryEditorExpressionType.FunctionParameter);
if (column?.name) {
return toOption(column.name);
}
return null;
}
@@ -1,68 +0,0 @@
import React from 'react';
import { useAsync } from 'react-use';
import { EditorRows, EditorRow, EditorField } from '@grafana/ui';
import { DB, QueryEditorProps, QueryRowFilter } from '../../types';
import { QueryToolbox } from '../query-editor-raw/QueryToolbox';
import { Preview } from './Preview';
import { SQLGroupByRow } from './SQLGroupByRow';
import { SQLOrderByRow } from './SQLOrderByRow';
import { SQLSelectRow } from './SQLSelectRow';
import { SQLWhereRow } from './SQLWhereRow';
interface VisualEditorProps extends QueryEditorProps {
db: DB;
queryRowFilter: QueryRowFilter;
onValidate: (isValid: boolean) => void;
}
export const VisualEditor: React.FC<VisualEditorProps> = ({
query,
db,
queryRowFilter,
onChange,
onValidate,
range,
}) => {
const state = useAsync(async () => {
const fields = await db.fields(query);
return fields;
}, [db, query.dataset, query.table]);
return (
<>
<EditorRows>
<EditorRow>
<SQLSelectRow fields={state.value || []} query={query} onQueryChange={onChange} db={db} />
</EditorRow>
{queryRowFilter.filter && (
<EditorRow>
<EditorField label="Filter by column value" optional>
<SQLWhereRow fields={state.value || []} query={query} onQueryChange={onChange} db={db} />
</EditorField>
</EditorRow>
)}
{queryRowFilter.group && (
<EditorRow>
<EditorField label="Group by column">
<SQLGroupByRow fields={state.value || []} query={query} onQueryChange={onChange} db={db} />
</EditorField>
</EditorRow>
)}
{queryRowFilter.order && (
<EditorRow>
<SQLOrderByRow fields={state.value || []} query={query} onQueryChange={onChange} db={db} />
</EditorRow>
)}
{queryRowFilter.preview && query.rawSql && (
<EditorRow>
<Preview rawSql={query.rawSql} />
</EditorRow>
)}
</EditorRows>
<QueryToolbox db={db} query={query} onValidate={onValidate} range={range} />
</>
);
};
@@ -1,92 +0,0 @@
import { injectGlobal } from '@emotion/css';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Builder, Config, ImmutableTree, Query, Utils } from 'react-awesome-query-builder';
import { SQLExpression } from '../../types';
import { emptyInitTree, raqbConfig } from './AwesomeQueryBuilder';
interface SQLBuilderWhereRowProps {
sql: SQLExpression;
onSqlChange: (sql: SQLExpression) => void;
config?: Partial<Config>;
}
export function WhereRow({ sql, config, onSqlChange }: SQLBuilderWhereRowProps) {
const [tree, setTree] = useState<ImmutableTree>();
const configWithDefaults = useMemo(() => ({ ...raqbConfig, ...config }), [config]);
useEffect(() => {
// Set the initial tree
if (!tree) {
const initTree = Utils.checkTree(Utils.loadTree(sql.whereJsonTree ?? emptyInitTree), configWithDefaults);
setTree(initTree);
}
}, [configWithDefaults, sql.whereJsonTree, tree]);
useEffect(() => {
if (!sql.whereJsonTree) {
setTree(Utils.checkTree(Utils.loadTree(emptyInitTree), configWithDefaults));
}
}, [configWithDefaults, sql.whereJsonTree]);
const onTreeChange = useCallback(
(changedTree: ImmutableTree, config: Config) => {
setTree(changedTree);
const newSql = {
...sql,
whereJsonTree: Utils.getTree(changedTree),
whereString: Utils.sqlFormat(changedTree, config),
};
onSqlChange(newSql);
},
[onSqlChange, sql]
);
if (!tree) {
return null;
}
return (
<Query
{...configWithDefaults}
value={tree}
onChange={onTreeChange}
renderBuilder={(props) => <Builder {...props} />}
/>
);
}
function flex(direction: string) {
return `
display: flex;
gap: 8px;
flex-direction: ${direction};`;
}
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
injectGlobal`
.group--header {
${flex('row')}
}
.group-or-rule {
${flex('column')}
.rule {
flex-direction: row;
}
}
.rule--body {
${flex('row')}
}
.group--children {
${flex('column')}
}
.group--conjunctions:empty {
display: none;
}
`;
@@ -1 +0,0 @@
export { GroupByRow } from './GroupByRow';
@@ -1,128 +0,0 @@
import { OperatorType } from './types';
export const AGGREGATE_FNS = [
{
id: 'AVG',
name: 'AVG',
description: `AVG(
[DISTINCT]
expression
)
[OVER (...)]
Returns the average of non-NULL input values, or NaN if the input contains a NaN.`,
},
{
id: 'COUNT',
name: 'COUNT',
description: `COUNT(*) [OVER (...)]
Returns the number of rows in the input.
COUNT(
[DISTINCT]
expression
)
[OVER (...)]
Returns the number of rows with expression evaluated to any value other than NULL.
`,
},
{
id: 'MAX',
name: 'MAX',
description: `MAX(
expression
)
[OVER (...)]
Returns the maximum value of non-NULL expressions. Returns NULL if there are zero input rows or expression evaluates to NULL for all rows. Returns NaN if the input contains a NaN.
`,
},
{
id: 'MIN',
name: 'MIN',
description: `MIN(
expression
)
[OVER (...)]
Returns the minimum value of non-NULL expressions. Returns NULL if there are zero input rows or expression evaluates to NULL for all rows. Returns NaN if the input contains a NaN.
`,
},
{
id: 'SUM',
name: 'SUM',
description: `SUM(
[DISTINCT]
expression
)
[OVER (...)]
Returns the sum of non-null values.
If the expression is a floating point value, the sum is non-deterministic, which means you might receive a different result each time you use this function.
`,
},
];
export const OPERATORS = [
{ type: OperatorType.Comparison, id: 'LESS_THAN', operator: '<', description: 'Returns TRUE if X is less than Y.' },
{
type: OperatorType.Comparison,
id: 'LESS_THAN_EQUAL',
operator: '<=',
description: 'Returns TRUE if X is less than or equal to Y.',
},
{
type: OperatorType.Comparison,
id: 'GREATER_THAN',
operator: '>',
description: 'Returns TRUE if X is greater than Y.',
},
{
type: OperatorType.Comparison,
id: 'GREATER_THAN_EQUAL',
operator: '>=',
description: 'Returns TRUE if X is greater than or equal to Y.',
},
{ type: OperatorType.Comparison, id: 'EQUAL', operator: '=', description: 'Returns TRUE if X is equal to Y.' },
{
type: OperatorType.Comparison,
id: 'NOT_EQUAL',
operator: '!=',
description: 'Returns TRUE if X is not equal to Y.',
},
{
type: OperatorType.Comparison,
id: 'NOT_EQUAL_ALT',
operator: '<>',
description: 'Returns TRUE if X is not equal to Y.',
},
{
type: OperatorType.Comparison,
id: 'LIKE',
operator: 'LIKE',
description: `Checks if the STRING in the first operand X matches a pattern specified by the second operand Y. Expressions can contain these characters:
- A percent sign "%" matches any number of characters or bytes
- An underscore "_" matches a single character or byte
- You can escape "\", "_", or "%" using two backslashes. For example, "\\%". If you are using raw strings, only a single backslash is required. For example, r"\%".`,
},
{ type: OperatorType.Logical, id: 'AND', operator: 'AND' },
{ type: OperatorType.Logical, id: 'OR', operator: 'OR' },
];
export const MACRO_NAMES = [
'$__time',
'$__timeEpoch',
'$__timeFilter',
'$__timeFrom',
'$__timeTo',
'$__timeGroup',
'$__timeGroupAlias',
'$__unixEpochFilter',
'$__unixEpochNanoFilter',
'$__unixEpochNanoFrom',
'$__unixEpochNanoTo',
'$__unixEpochGroup',
'$__unixEpochGroupAlias',
];
@@ -1,210 +0,0 @@
import { lastValueFrom, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import {
DataFrame,
DataFrameView,
DataQuery,
DataSourceInstanceSettings,
DataSourceRef,
MetricFindValue,
ScopedVars,
TimeRange,
} from '@grafana/data';
import {
BackendDataSourceResponse,
DataSourceWithBackend,
FetchResponse,
getBackendSrv,
getTemplateSrv,
TemplateSrv,
} from '@grafana/runtime';
import { toDataQueryResponse, toTestingStatus } from '@grafana/runtime/src/utils/queryResponse';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { VariableWithMultiSupport } from '../../../variables/types';
import { getSearchFilterScopedVar, SearchFilterOptions } from '../../../variables/utils';
import { MACRO_NAMES } from '../constants';
import { DB, SQLQuery, SQLOptions, ResponseParser, SqlQueryModel, QueryFormat } from '../types';
export abstract class SqlDatasource extends DataSourceWithBackend<SQLQuery, SQLOptions> {
id: number;
name: string;
interval: string;
db: DB;
annotations = {};
constructor(
instanceSettings: DataSourceInstanceSettings<SQLOptions>,
protected readonly templateSrv: TemplateSrv = getTemplateSrv()
) {
super(instanceSettings);
this.name = instanceSettings.name;
this.id = instanceSettings.id;
const settingsData = instanceSettings.jsonData || {};
this.interval = settingsData.timeInterval || '1m';
this.db = this.getDB();
}
abstract getDB(dsID?: number): DB;
abstract getQueryModel(target?: SQLQuery, templateSrv?: TemplateSrv, scopedVars?: ScopedVars): SqlQueryModel;
abstract getResponseParser(): ResponseParser;
interpolateVariable = (value: string | string[] | number, variable: VariableWithMultiSupport) => {
if (typeof value === 'string') {
if (variable.multi || variable.includeAll) {
const result = this.getQueryModel().quoteLiteral(value);
return result;
} else {
return value;
}
}
if (typeof value === 'number') {
return value;
}
if (Array.isArray(value)) {
const quotedValues = value.map((v) => this.getQueryModel().quoteLiteral(v));
return quotedValues.join(',');
}
return value;
};
interpolateVariablesInQueries(queries: SQLQuery[], scopedVars: ScopedVars): SQLQuery[] {
let expandedQueries = queries;
if (queries && queries.length > 0) {
expandedQueries = queries.map((query) => {
const expandedQuery = {
...query,
datasource: this.getRef(),
rawSql: this.templateSrv.replace(query.rawSql, scopedVars, this.interpolateVariable),
rawQuery: true,
};
return expandedQuery;
});
}
return expandedQueries;
}
filterQuery(query: SQLQuery): boolean {
return !query.hide;
}
applyTemplateVariables(
target: SQLQuery,
scopedVars: ScopedVars
): Record<string, string | DataSourceRef | SQLQuery['format']> {
const queryModel = this.getQueryModel(target, this.templateSrv, scopedVars);
const rawSql = this.clean(queryModel.interpolate());
return {
refId: target.refId,
datasource: this.getRef(),
rawSql,
format: target.format,
};
}
clean(value: string) {
return value.replace(/''/g, "'");
}
async metricFindQuery(query: string, optionalOptions?: MetricFindQueryOptions): Promise<MetricFindValue[]> {
const rawSql = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '%', options: optionalOptions }),
this.interpolateVariable
);
const interpolatedQuery: SQLQuery = {
refId: 'tempvar',
datasource: this.getRef(),
rawSql,
format: QueryFormat.Table,
};
const response = await this.runMetaQuery(interpolatedQuery, optionalOptions);
return this.getResponseParser().transformMetricFindResponse(response);
}
async runSql<T>(query: string, options?: RunSQLOptions) {
const frame = await this.runMetaQuery({ rawSql: query, format: QueryFormat.Table, refId: options?.refId }, options);
return new DataFrameView<T>(frame);
}
private runMetaQuery(request: Partial<SQLQuery>, options?: MetricFindQueryOptions): Promise<DataFrame> {
const range = getTimeSrv().timeRange();
const refId = request.refId || 'meta';
const queries: DataQuery[] = [{ ...request, datasource: request.datasource || this.getRef(), refId }];
return lastValueFrom(
getBackendSrv()
.fetch<BackendDataSourceResponse>({
url: '/api/ds/query',
method: 'POST',
data: {
from: options?.range?.from.valueOf().toString() || range.from.valueOf().toString(),
to: options?.range?.to.valueOf().toString() || range.to.valueOf().toString(),
queries,
},
requestId: refId,
})
.pipe(
map((res: FetchResponse<BackendDataSourceResponse>) => {
const rsp = toDataQueryResponse(res, queries);
return rsp.data[0];
})
)
);
}
testDatasource(): Promise<{ status: string; message: string }> {
return lastValueFrom(
getBackendSrv()
.fetch({
url: '/api/ds/query',
method: 'POST',
data: {
from: '5m',
to: 'now',
queries: [
{
refId: 'A',
intervalMs: 1,
maxDataPoints: 1,
datasource: this.getRef(),
datasourceId: this.id,
rawSql: 'SELECT 1',
format: 'table',
},
],
},
})
.pipe(
map(() => ({ status: 'success', message: 'Database Connection OK' })),
catchError((err) => {
return of(toTestingStatus(err));
})
)
);
}
targetContainsTemplate(target: SQLQuery) {
let queryWithoutMacros = target.rawSql;
MACRO_NAMES.forEach((value) => {
queryWithoutMacros = queryWithoutMacros?.replace(value, '') || '';
});
return this.templateSrv.containsTemplate(queryWithoutMacros);
}
}
interface RunSQLOptions extends MetricFindQueryOptions {
refId?: string;
}
interface MetricFindQueryOptions extends SearchFilterOptions {
range?: TimeRange;
}
@@ -1,28 +0,0 @@
import { EditorMode, QueryFormat, SQLQuery } from './types';
import { createFunctionField, setGroupByField } from './utils/sql.utils';
export function applyQueryDefaults(q?: SQLQuery): SQLQuery {
let editorMode = q?.editorMode || EditorMode.Builder;
// Switching to code editor if the query was created before visual query builder was introduced.
if (q?.editorMode === undefined && q?.rawSql !== undefined) {
editorMode = EditorMode.Code;
}
const result: SQLQuery = {
...q,
refId: q?.refId || 'A',
format: q?.format !== undefined ? q.format : QueryFormat.Table,
rawSql: q?.rawSql || '',
editorMode,
sql: q?.sql || {
columns: [createFunctionField()],
groupBy: [setGroupByField()],
limit: 50,
},
};
return result;
}
export type QueryWithDefaults = ReturnType<typeof applyQueryDefaults>;
@@ -1,66 +0,0 @@
export enum QueryEditorPropertyType {
String = 'string',
}
export interface QueryEditorProperty {
type: QueryEditorPropertyType;
name?: string;
}
export type QueryEditorOperatorType = string | boolean | number;
type QueryEditorOperatorValueType = QueryEditorOperatorType | QueryEditorOperatorType[];
export interface QueryEditorOperator<T extends QueryEditorOperatorValueType> {
name?: string;
value?: T;
}
export interface QueryEditorOperatorExpression {
type: QueryEditorExpressionType.Operator;
property: QueryEditorProperty;
operator: QueryEditorOperator<QueryEditorOperatorValueType>;
}
export interface QueryEditorArrayExpression {
type: QueryEditorExpressionType.And | QueryEditorExpressionType.Or;
expressions: QueryEditorExpression[] | QueryEditorArrayExpression[];
}
export interface QueryEditorPropertyExpression {
type: QueryEditorExpressionType.Property;
property: QueryEditorProperty;
}
export enum QueryEditorExpressionType {
Property = 'property',
Operator = 'operator',
Or = 'or',
And = 'and',
GroupBy = 'groupBy',
Function = 'function',
FunctionParameter = 'functionParameter',
}
export type QueryEditorExpression =
| QueryEditorArrayExpression
| QueryEditorPropertyExpression
| QueryEditorGroupByExpression
| QueryEditorFunctionExpression
| QueryEditorFunctionParameterExpression
| QueryEditorOperatorExpression;
export interface QueryEditorGroupByExpression {
type: QueryEditorExpressionType.GroupBy;
property: QueryEditorProperty;
}
export interface QueryEditorFunctionExpression {
type: QueryEditorExpressionType.Function;
name?: string;
parameters?: QueryEditorFunctionParameterExpression[];
}
export interface QueryEditorFunctionParameterExpression {
type: QueryEditorExpressionType.FunctionParameter;
name?: string;
}
-22
View File
@@ -1,22 +0,0 @@
export { SQLEditorTestUtils, TestQueryModel } from './test-utils';
export { LinkedToken } from './utils/LinkedToken';
export { language as grafanaStandardSQLLanguage, conf as grafanaStandardSQLLanguageConf } from './standardSql/language';
export { SQLMonarchLanguage } from './standardSql/types';
export {
TableDefinition,
ColumnDefinition,
StatementPlacementProvider,
SuggestionKindProvider,
LanguageCompletionProvider,
OperatorType,
MacroType,
TokenType,
StatementPosition,
SuggestionKind,
CompletionItemKind,
CompletionItemPriority,
CompletionItemInsertTextRule,
} from './types';
export * from './components';
@@ -1,26 +0,0 @@
import { monacoTypes } from '@grafana/ui';
// Stub for the Monaco instance. Only implements the parts that are used in cloudwatch sql
const getMonacoMock: (
testData: Map<string, Array<Array<Pick<monacoTypes.Token, 'language' | 'offset' | 'type'>>>>
) => any = (testData) => ({
editor: {
tokenize: (value: string, languageId: string) => testData.get(value),
},
Range: {
containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => {
return (
position.lineNumber >= range.startLineNumber &&
position.lineNumber <= range.endLineNumber &&
position.column >= range.startColumn &&
position.column <= range.endColumn
);
},
},
languages: {
CompletionItemKind: { Snippet: 2, Function: 1, Keyword: 3 },
CompletionItemInsertTextRule: { InsertAsSnippet: 2 },
},
});
export { getMonacoMock };
@@ -1,21 +0,0 @@
import { monacoTypes } from '@grafana/ui';
// Stub for monacoTypes.editor.ITextModel
function TextModel(value: string) {
return {
getValue: function (eol?: monacoTypes.editor.EndOfLinePreference, preserveBOM?: boolean): string {
return value;
},
getValueInRange: function (range: monacoTypes.IRange, eol?: monacoTypes.editor.EndOfLinePreference): string {
const lines = value.split('\n');
const line = lines[range.startLineNumber - 1];
return line.trim().slice(range.startColumn === 0 ? 0 : range.startColumn - 1, range.endColumn - 1);
},
getLineLength: function (lineNumber: number): number {
const lines = value.split('\n');
return lines[lineNumber - 1].trim().length;
},
};
}
export { TextModel };
@@ -1,214 +0,0 @@
import { TestQueryModel } from '../../test-utils/types';
export const multiLineFullQuery: TestQueryModel = {
query: `SELECT column1,
FROM table1
WHERE column1 = "value1"
GROUP BY column1 ORDER BY column1 DESC
LIMIT 10;`,
tokens: [
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 6,
type: 'white.sql',
language: 'sql',
},
{
offset: 7,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 14,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 15,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 4,
type: 'white.sql',
language: 'sql',
},
{
offset: 5,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 11,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 13,
type: 'white.sql',
language: 'sql',
},
{
offset: 14,
type: 'operator.sql',
language: 'sql',
},
{
offset: 15,
type: 'white.sql',
language: 'sql',
},
{
offset: 16,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 17,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 23,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 24,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 8,
type: 'white.sql',
language: 'sql',
},
{
offset: 9,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 16,
type: 'white.sql',
language: 'sql',
},
{
offset: 17,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 22,
type: 'white.sql',
language: 'sql',
},
{
offset: 23,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 25,
type: 'white.sql',
language: 'sql',
},
{
offset: 26,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 33,
type: 'white.sql',
language: 'sql',
},
{
offset: 34,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 38,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'number.sql',
language: 'sql',
},
{
offset: 8,
type: 'delimiter.sql',
language: 'sql',
},
],
],
};
@@ -1,229 +0,0 @@
import { TestQueryModel } from '../../test-utils/types';
export const multiLineFullQueryWithAggregation: TestQueryModel = {
query: `SELECT count(column1),
FROM table1
WHERE column1 = "value1"
GROUP BY column1 ORDER BY column1 DESC
LIMIT 10;`,
tokens: [
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 6,
type: 'white.sql',
language: 'sql',
},
{
offset: 7,
type: 'predefined.sql',
language: 'sql',
},
{
offset: 12,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 13,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 20,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 21,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 22,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 4,
type: 'white.sql',
language: 'sql',
},
{
offset: 5,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 11,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 13,
type: 'white.sql',
language: 'sql',
},
{
offset: 14,
type: 'operator.sql',
language: 'sql',
},
{
offset: 15,
type: 'white.sql',
language: 'sql',
},
{
offset: 16,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 17,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 23,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 24,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 8,
type: 'white.sql',
language: 'sql',
},
{
offset: 9,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 16,
type: 'white.sql',
language: 'sql',
},
{
offset: 17,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 22,
type: 'white.sql',
language: 'sql',
},
{
offset: 23,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 25,
type: 'white.sql',
language: 'sql',
},
{
offset: 26,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 33,
type: 'white.sql',
language: 'sql',
},
{
offset: 34,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 38,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'number.sql',
language: 'sql',
},
{
offset: 8,
type: 'delimiter.sql',
language: 'sql',
},
],
],
};
@@ -1,269 +0,0 @@
import { TestQueryModel } from '../../test-utils/types';
export const multiLineMultipleColumns: TestQueryModel = {
query: `SELECT count(column1), column2
FROM table1
WHERE column1 = "value1"
GROUP BY column1 ORDER BY column1, avg(column2) DESC
LIMIT 10;`,
tokens: [
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 6,
type: 'white.sql',
language: 'sql',
},
{
offset: 7,
type: 'predefined.sql',
language: 'sql',
},
{
offset: 12,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 13,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 20,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 21,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 22,
type: 'white.sql',
language: 'sql',
},
{
offset: 23,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 30,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 4,
type: 'white.sql',
language: 'sql',
},
{
offset: 5,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 11,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 13,
type: 'white.sql',
language: 'sql',
},
{
offset: 14,
type: 'operator.sql',
language: 'sql',
},
{
offset: 15,
type: 'white.sql',
language: 'sql',
},
{
offset: 16,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 17,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 23,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 24,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 8,
type: 'white.sql',
language: 'sql',
},
{
offset: 9,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 16,
type: 'white.sql',
language: 'sql',
},
{
offset: 17,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 22,
type: 'white.sql',
language: 'sql',
},
{
offset: 23,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 25,
type: 'white.sql',
language: 'sql',
},
{
offset: 26,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 33,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 34,
type: 'white.sql',
language: 'sql',
},
{
offset: 35,
type: 'predefined.sql',
language: 'sql',
},
{
offset: 38,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 39,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 46,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 47,
type: 'white.sql',
language: 'sql',
},
{
offset: 48,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 52,
type: 'white.sql',
language: 'sql',
},
],
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 5,
type: 'white.sql',
language: 'sql',
},
{
offset: 6,
type: 'number.sql',
language: 'sql',
},
{
offset: 8,
type: 'delimiter.sql',
language: 'sql',
},
],
],
};
@@ -1,6 +0,0 @@
import { TestQueryModel } from '../../test-utils/types';
export const singleLineEmptyQuery: TestQueryModel = {
query: '',
tokens: [],
};
@@ -1,196 +0,0 @@
import { monacoTypes } from '@grafana/ui';
import { TestQueryModel } from '../../test-utils/types';
export const singleLineFullQuery: TestQueryModel = {
query: `SELECT column1, FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1 DESC LIMIT 10`,
tokens: [
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 6,
type: 'white.sql',
language: 'sql',
},
{
offset: 7,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 14,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 15,
type: 'white.sql',
language: 'sql',
},
{
offset: 16,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 20,
type: 'white.sql',
language: 'sql',
},
{
offset: 21,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 27,
type: 'white.sql',
language: 'sql',
},
{
offset: 28,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 33,
type: 'white.sql',
language: 'sql',
},
{
offset: 34,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 41,
type: 'white.sql',
language: 'sql',
},
{
offset: 42,
type: 'operator.sql',
language: 'sql',
},
{
offset: 43,
type: 'white.sql',
language: 'sql',
},
{
offset: 44,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 45,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 51,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 52,
type: 'white.sql',
language: 'sql',
},
{
offset: 53,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 58,
type: 'white.sql',
language: 'sql',
},
{
offset: 59,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 61,
type: 'white.sql',
language: 'sql',
},
{
offset: 62,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 69,
type: 'white.sql',
language: 'sql',
},
{
offset: 70,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 75,
type: 'white.sql',
language: 'sql',
},
{
offset: 76,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 78,
type: 'white.sql',
language: 'sql',
},
{
offset: 79,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 86,
type: 'white.sql',
language: 'sql',
},
{
offset: 87,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 91,
type: 'white.sql',
language: 'sql',
},
{
offset: 92,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 97,
type: 'white.sql',
language: 'sql',
},
{
offset: 98,
type: 'number.sql',
language: 'sql',
},
{
offset: 100,
type: 'delimiter.sql',
language: 'sql',
},
],
] as monacoTypes.Token[][],
};
@@ -1,209 +0,0 @@
import { TestQueryModel } from '../../test-utils/types';
export const singleLineFullQueryWithAggregation: TestQueryModel = {
query: 'SELECT count(column1), FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1 DESC LIMIT 10;',
tokens: [
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 6,
type: 'white.sql',
language: 'sql',
},
{
offset: 7,
type: 'predefined.sql',
language: 'sql',
},
{
offset: 12,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 13,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 20,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 21,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 22,
type: 'white.sql',
language: 'sql',
},
{
offset: 23,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 27,
type: 'white.sql',
language: 'sql',
},
{
offset: 28,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 34,
type: 'white.sql',
language: 'sql',
},
{
offset: 35,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 40,
type: 'white.sql',
language: 'sql',
},
{
offset: 41,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 48,
type: 'white.sql',
language: 'sql',
},
{
offset: 49,
type: 'operator.sql',
language: 'sql',
},
{
offset: 50,
type: 'white.sql',
language: 'sql',
},
{
offset: 51,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 52,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 58,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 59,
type: 'white.sql',
language: 'sql',
},
{
offset: 60,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 65,
type: 'white.sql',
language: 'sql',
},
{
offset: 66,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 68,
type: 'white.sql',
language: 'sql',
},
{
offset: 69,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 76,
type: 'white.sql',
language: 'sql',
},
{
offset: 77,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 82,
type: 'white.sql',
language: 'sql',
},
{
offset: 83,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 85,
type: 'white.sql',
language: 'sql',
},
{
offset: 86,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 93,
type: 'white.sql',
language: 'sql',
},
{
offset: 94,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 98,
type: 'white.sql',
language: 'sql',
},
{
offset: 99,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 104,
type: 'white.sql',
language: 'sql',
},
{
offset: 105,
type: 'number.sql',
language: 'sql',
},
{
offset: 107,
type: 'delimiter.sql',
language: 'sql',
},
],
],
};
@@ -1,250 +0,0 @@
import { TestQueryModel } from '../../test-utils/types';
export const singleLineMultipleColumns: TestQueryModel = {
query:
'SELECT count(column1), column2 FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1, avg(column2) DESC LIMIT 10;',
tokens: [
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 6,
type: 'white.sql',
language: 'sql',
},
{
offset: 7,
type: 'predefined.sql',
language: 'sql',
},
{
offset: 12,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 13,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 20,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 21,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 22,
type: 'white.sql',
language: 'sql',
},
{
offset: 23,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 30,
type: 'white.sql',
language: 'sql',
},
{
offset: 31,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 35,
type: 'white.sql',
language: 'sql',
},
{
offset: 36,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 42,
type: 'white.sql',
language: 'sql',
},
{
offset: 43,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 48,
type: 'white.sql',
language: 'sql',
},
{
offset: 49,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 56,
type: 'white.sql',
language: 'sql',
},
{
offset: 57,
type: 'operator.sql',
language: 'sql',
},
{
offset: 58,
type: 'white.sql',
language: 'sql',
},
{
offset: 59,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 60,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 66,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 67,
type: 'white.sql',
language: 'sql',
},
{
offset: 68,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 73,
type: 'white.sql',
language: 'sql',
},
{
offset: 74,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 76,
type: 'white.sql',
language: 'sql',
},
{
offset: 77,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 84,
type: 'white.sql',
language: 'sql',
},
{
offset: 85,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 90,
type: 'white.sql',
language: 'sql',
},
{
offset: 91,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 93,
type: 'white.sql',
language: 'sql',
},
{
offset: 94,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 101,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 102,
type: 'white.sql',
language: 'sql',
},
{
offset: 103,
type: 'predefined.sql',
language: 'sql',
},
{
offset: 106,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 107,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 114,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 115,
type: 'white.sql',
language: 'sql',
},
{
offset: 116,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 120,
type: 'white.sql',
language: 'sql',
},
{
offset: 121,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 126,
type: 'white.sql',
language: 'sql',
},
{
offset: 127,
type: 'number.sql',
language: 'sql',
},
{
offset: 129,
type: 'delimiter.sql',
language: 'sql',
},
],
],
};
@@ -1,385 +0,0 @@
import { TestQueryModel } from '../../test-utils/types';
export const singleLineTwoQueries: TestQueryModel = {
query:
'SELECT column1, FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1 DESC LIMIT 10; SELECT column2, FROM table2 WHERE column2 = "value2" GROUP BY column1 ORDER BY column2 DESC LIMIT 10;',
tokens: [
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 6,
type: 'white.sql',
language: 'sql',
},
{
offset: 7,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 14,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 15,
type: 'white.sql',
language: 'sql',
},
{
offset: 16,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 20,
type: 'white.sql',
language: 'sql',
},
{
offset: 21,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 27,
type: 'white.sql',
language: 'sql',
},
{
offset: 28,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 33,
type: 'white.sql',
language: 'sql',
},
{
offset: 34,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 41,
type: 'white.sql',
language: 'sql',
},
{
offset: 42,
type: 'operator.sql',
language: 'sql',
},
{
offset: 43,
type: 'white.sql',
language: 'sql',
},
{
offset: 44,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 45,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 51,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 52,
type: 'white.sql',
language: 'sql',
},
{
offset: 53,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 58,
type: 'white.sql',
language: 'sql',
},
{
offset: 59,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 61,
type: 'white.sql',
language: 'sql',
},
{
offset: 62,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 69,
type: 'white.sql',
language: 'sql',
},
{
offset: 70,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 75,
type: 'white.sql',
language: 'sql',
},
{
offset: 76,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 78,
type: 'white.sql',
language: 'sql',
},
{
offset: 79,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 86,
type: 'white.sql',
language: 'sql',
},
{
offset: 87,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 91,
type: 'white.sql',
language: 'sql',
},
{
offset: 92,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 97,
type: 'white.sql',
language: 'sql',
},
{
offset: 98,
type: 'number.sql',
language: 'sql',
},
{
offset: 100,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 101,
type: 'white.sql',
language: 'sql',
},
{
offset: 102,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 108,
type: 'white.sql',
language: 'sql',
},
{
offset: 109,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 116,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 117,
type: 'white.sql',
language: 'sql',
},
{
offset: 118,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 122,
type: 'white.sql',
language: 'sql',
},
{
offset: 123,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 129,
type: 'white.sql',
language: 'sql',
},
{
offset: 130,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 135,
type: 'white.sql',
language: 'sql',
},
{
offset: 136,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 143,
type: 'white.sql',
language: 'sql',
},
{
offset: 144,
type: 'operator.sql',
language: 'sql',
},
{
offset: 145,
type: 'white.sql',
language: 'sql',
},
{
offset: 146,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 147,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 153,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 154,
type: 'white.sql',
language: 'sql',
},
{
offset: 155,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 160,
type: 'white.sql',
language: 'sql',
},
{
offset: 161,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 163,
type: 'white.sql',
language: 'sql',
},
{
offset: 164,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 171,
type: 'white.sql',
language: 'sql',
},
{
offset: 172,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 177,
type: 'white.sql',
language: 'sql',
},
{
offset: 178,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 180,
type: 'white.sql',
language: 'sql',
},
{
offset: 181,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 188,
type: 'white.sql',
language: 'sql',
},
{
offset: 189,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 193,
type: 'white.sql',
language: 'sql',
},
{
offset: 194,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 199,
type: 'white.sql',
language: 'sql',
},
{
offset: 200,
type: 'number.sql',
language: 'sql',
},
{
offset: 202,
type: 'delimiter.sql',
language: 'sql',
},
],
],
};
@@ -1,415 +0,0 @@
import { TestQueryModel } from '../../test-utils/types';
export const singleLineTwoQueriesWithAggregation: TestQueryModel = {
query:
'SELECT count(column1), FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1 DESC LIMIT 10; SELECT count(column2), FROM table2 WHERE column2 = "value2" GROUP BY column1 ORDER BY column2 DESC LIMIT 10;',
tokens: [
[
{
offset: 0,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 6,
type: 'white.sql',
language: 'sql',
},
{
offset: 7,
type: 'predefined.sql',
language: 'sql',
},
{
offset: 12,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 13,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 20,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 21,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 22,
type: 'white.sql',
language: 'sql',
},
{
offset: 23,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 27,
type: 'white.sql',
language: 'sql',
},
{
offset: 28,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 34,
type: 'white.sql',
language: 'sql',
},
{
offset: 35,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 40,
type: 'white.sql',
language: 'sql',
},
{
offset: 41,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 48,
type: 'white.sql',
language: 'sql',
},
{
offset: 49,
type: 'operator.sql',
language: 'sql',
},
{
offset: 50,
type: 'white.sql',
language: 'sql',
},
{
offset: 51,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 52,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 58,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 59,
type: 'white.sql',
language: 'sql',
},
{
offset: 60,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 65,
type: 'white.sql',
language: 'sql',
},
{
offset: 66,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 68,
type: 'white.sql',
language: 'sql',
},
{
offset: 69,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 76,
type: 'white.sql',
language: 'sql',
},
{
offset: 77,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 82,
type: 'white.sql',
language: 'sql',
},
{
offset: 83,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 85,
type: 'white.sql',
language: 'sql',
},
{
offset: 86,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 93,
type: 'white.sql',
language: 'sql',
},
{
offset: 94,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 98,
type: 'white.sql',
language: 'sql',
},
{
offset: 99,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 104,
type: 'white.sql',
language: 'sql',
},
{
offset: 105,
type: 'number.sql',
language: 'sql',
},
{
offset: 107,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 108,
type: 'white.sql',
language: 'sql',
},
{
offset: 109,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 115,
type: 'white.sql',
language: 'sql',
},
{
offset: 116,
type: 'predefined.sql',
language: 'sql',
},
{
offset: 121,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 122,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 129,
type: 'delimiter.parenthesis.sql',
language: 'sql',
},
{
offset: 130,
type: 'delimiter.sql',
language: 'sql',
},
{
offset: 131,
type: 'white.sql',
language: 'sql',
},
{
offset: 132,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 136,
type: 'white.sql',
language: 'sql',
},
{
offset: 137,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 143,
type: 'white.sql',
language: 'sql',
},
{
offset: 144,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 149,
type: 'white.sql',
language: 'sql',
},
{
offset: 150,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 157,
type: 'white.sql',
language: 'sql',
},
{
offset: 158,
type: 'operator.sql',
language: 'sql',
},
{
offset: 159,
type: 'white.sql',
language: 'sql',
},
{
offset: 160,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 161,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 167,
type: 'identifier.quote.sql',
language: 'sql',
},
{
offset: 168,
type: 'white.sql',
language: 'sql',
},
{
offset: 169,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 174,
type: 'white.sql',
language: 'sql',
},
{
offset: 175,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 177,
type: 'white.sql',
language: 'sql',
},
{
offset: 178,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 185,
type: 'white.sql',
language: 'sql',
},
{
offset: 186,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 191,
type: 'white.sql',
language: 'sql',
},
{
offset: 192,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 194,
type: 'white.sql',
language: 'sql',
},
{
offset: 195,
type: 'identifier.sql',
language: 'sql',
},
{
offset: 202,
type: 'white.sql',
language: 'sql',
},
{
offset: 203,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 207,
type: 'white.sql',
language: 'sql',
},
{
offset: 208,
type: 'keyword.sql',
language: 'sql',
},
{
offset: 213,
type: 'white.sql',
language: 'sql',
},
{
offset: 214,
type: 'number.sql',
language: 'sql',
},
{
offset: 216,
type: 'delimiter.sql',
language: 'sql',
},
],
],
};
@@ -1,9 +0,0 @@
export { singleLineFullQuery } from './queries/singleLineFullQuery';
export { singleLineFullQueryWithAggregation } from './queries/singleLineFullQueryWithAggregation';
export { multiLineFullQuery } from './queries/multiLineFullQuery';
export { multiLineFullQueryWithAggregation } from './queries/multiLineFullQueryWithAggregation';
export { singleLineEmptyQuery } from './queries/singleLineEmptyQuery';
export { singleLineTwoQueries } from './queries/singleLineTwoQueries';
export { singleLineTwoQueriesWithAggregation } from './queries/singleLineTwoQueriesWithAggregation';
export { singleLineMultipleColumns } from './queries/singleLineMultipleColumns';
export { multiLineMultipleColumns } from './queries/multiLineMultipleColumns';
@@ -1,24 +0,0 @@
import { monacoTypes } from '@grafana/ui';
import { SQLMonarchLanguage } from './types';
export type LanguageDefinition = {
id: string;
extensions: string[];
aliases: string[];
mimetypes: string[];
loader: (monaco: any) => Promise<{
language: SQLMonarchLanguage;
conf: monacoTypes.languages.LanguageConfiguration;
}>;
};
const standardSQLLanguageDefinition: LanguageDefinition = {
id: 'standardSql',
extensions: ['.sql'],
aliases: ['sql'],
mimetypes: [],
loader: () => import('./language'),
};
export default standardSQLLanguageDefinition;
@@ -1,273 +0,0 @@
import { Registry } from '@grafana/data';
import { monacoTypes } from '@grafana/ui';
import { getMonacoMock } from '../mocks/Monaco';
import { TextModel } from '../mocks/TextModel';
import { singleLineFullQuery } from '../mocks/testData';
import { OperatorType, SuggestionKind, CustomSuggestion, PositionContext, MacroType } from '../types';
import { linkedTokenBuilder } from '../utils/linkedTokenBuilder';
import { getStandardSuggestions } from './getStandardSuggestions';
import { initStandardSuggestions } from './standardSuggestionsRegistry';
import { FunctionsRegistryItem, MacrosRegistryItem, OperatorsRegistryItem, SuggestionsRegistryItem } from './types';
describe('getStandardSuggestions', () => {
const mockQueries = new Map<string, Array<Array<Pick<monacoTypes.Token, 'language' | 'offset' | 'type'>>>>();
const cases = [{ query: singleLineFullQuery, position: { line: 1, column: 0 } }];
cases.forEach((c) => mockQueries.set(c.query.query, c.query.tokens));
const MonacoMock = getMonacoMock(mockQueries);
const token = linkedTokenBuilder(MonacoMock, TextModel(singleLineFullQuery.query) as monacoTypes.editor.ITextModel, {
lineNumber: 1,
column: 0,
});
const posContextMock = {};
it('calls the resolvers', async () => {
const suggestionMock: CustomSuggestion = { label: 'customSuggest' };
const resolveFunctionSpy = jest.fn().mockReturnValue([suggestionMock]);
const kind = 'customSuggestionItemKind' as SuggestionKind;
const suggestionsRegistry = new Registry<SuggestionsRegistryItem>(() => {
return [
{
id: kind,
name: 'customSuggestionItemKind',
suggestions: resolveFunctionSpy,
},
];
});
const result = await getStandardSuggestions(
MonacoMock,
token,
[kind],
posContextMock as PositionContext,
suggestionsRegistry
);
expect(resolveFunctionSpy).toBeCalledTimes(1);
expect(resolveFunctionSpy).toBeCalledWith({ range: token!.range }, MonacoMock);
expect(result).toHaveLength(1);
expect(result[0].label).toEqual(suggestionMock.label);
});
it('suggests custom functions with arguments from the registry', async () => {
const customFunction = {
name: 'customFunction',
id: 'customFunction',
};
const suggestionsRegistry = new Registry(
initStandardSuggestions(
new Registry<FunctionsRegistryItem>(() => [customFunction]),
new Registry<OperatorsRegistryItem>(() => []),
new Registry<MacrosRegistryItem>(() => [])
)
);
const result = await getStandardSuggestions(
MonacoMock,
token,
[SuggestionKind.FunctionsWithArguments],
posContextMock as PositionContext,
suggestionsRegistry
);
expect(result).toHaveLength(1);
expect(result[0].label).toEqual(customFunction.name);
});
it('suggests custom functions without arguments from the registry', async () => {
const customFunction = {
name: 'customFunction',
id: 'customFunction',
};
const suggestionsRegistry = new Registry(
initStandardSuggestions(
new Registry<FunctionsRegistryItem>(() => [customFunction]),
new Registry<OperatorsRegistryItem>(() => []),
new Registry<MacrosRegistryItem>(() => [])
)
);
const result = await getStandardSuggestions(
MonacoMock,
token,
[SuggestionKind.FunctionsWithoutArguments],
posContextMock as PositionContext,
suggestionsRegistry
);
expect(result).toHaveLength(1);
expect(result[0].label).toEqual(customFunction.name);
});
it('suggests custom logical operators from the registry', async () => {
const customLogicalOperator = {
type: OperatorType.Logical,
name: 'customOperator',
id: 'customOperator',
operator: '½',
};
const suggestionsRegistry = new Registry(
initStandardSuggestions(
new Registry<FunctionsRegistryItem>(() => []),
new Registry<OperatorsRegistryItem>(() => [customLogicalOperator]),
new Registry<MacrosRegistryItem>(() => [])
)
);
const result = await getStandardSuggestions(
MonacoMock,
token,
[SuggestionKind.LogicalOperators],
posContextMock as PositionContext,
suggestionsRegistry
);
expect(result).toHaveLength(1);
expect(result[0].label).toEqual(customLogicalOperator.operator);
});
it('suggests custom comparison operators from the registry', async () => {
const customComparisonOperator = {
type: OperatorType.Comparison,
name: 'customOperator',
id: 'customOperator',
operator: '§',
};
const suggestionsRegistry = new Registry(
initStandardSuggestions(
new Registry<FunctionsRegistryItem>(() => []),
new Registry<OperatorsRegistryItem>(() => [customComparisonOperator]),
new Registry<MacrosRegistryItem>(() => [])
)
);
const result = await getStandardSuggestions(
MonacoMock,
token,
[SuggestionKind.ComparisonOperators],
posContextMock as PositionContext,
suggestionsRegistry
);
expect(result).toHaveLength(5);
expect(result[0].label).toEqual(customComparisonOperator.operator);
});
it('does not suggest logical operators when asked for comparison operators', async () => {
const customLogicalOperator = {
type: OperatorType.Logical,
name: 'customOperator',
id: 'customOperator',
operator: '§',
};
const suggestionsRegistry = new Registry(
initStandardSuggestions(
new Registry<FunctionsRegistryItem>(() => []),
new Registry<OperatorsRegistryItem>(() => [customLogicalOperator]),
new Registry<MacrosRegistryItem>(() => [])
)
);
const result = await getStandardSuggestions(
MonacoMock,
token,
[SuggestionKind.ComparisonOperators],
posContextMock as PositionContext,
suggestionsRegistry
);
expect(result).toHaveLength(4);
});
it('suggests $__time(dateColumn) macro when in column position', async () => {
const customMacro: MacrosRegistryItem = {
name: '$__time',
id: '$__time',
text: '$__time',
type: MacroType.Value,
};
const suggestionsRegistry = new Registry(
initStandardSuggestions(
new Registry<FunctionsRegistryItem>(() => []),
new Registry<OperatorsRegistryItem>(() => []),
new Registry<MacrosRegistryItem>(() => [customMacro])
)
);
const result = await getStandardSuggestions(
MonacoMock,
token,
[SuggestionKind.SelectMacro],
posContextMock as PositionContext,
suggestionsRegistry
);
expect(result).toHaveLength(1);
expect(result[0].label).toEqual('$__time');
});
it('suggests SELECT and SELECT FROM from the standard registry', async () => {
const suggestionsRegistry = new Registry(
initStandardSuggestions(
new Registry<FunctionsRegistryItem>(() => []),
new Registry<OperatorsRegistryItem>(() => []),
new Registry<MacrosRegistryItem>(() => [])
)
);
const result = await getStandardSuggestions(
MonacoMock,
token,
[SuggestionKind.SelectKeyword],
posContextMock as PositionContext,
suggestionsRegistry
);
expect(result).toHaveLength(2);
expect(result).toMatchInlineSnapshot(`
Array [
Object {
"command": Object {
"id": "editor.action.triggerSuggest",
"title": "",
},
"insertText": "SELECT $0",
"insertTextRules": 4,
"kind": 27,
"label": "SELECT <column>",
"range": Object {
"endColumn": 7,
"endLineNumber": 1,
"startColumn": 0,
"startLineNumber": 1,
},
"sortText": "g",
},
Object {
"command": Object {
"id": "editor.action.triggerSuggest",
"title": "",
},
"insertText": "SELECT $2 FROM $1",
"insertTextRules": 4,
"kind": 27,
"label": "SELECT <column> FROM <table>",
"range": Object {
"endColumn": 7,
"endLineNumber": 1,
"startColumn": 0,
"startLineNumber": 1,
},
"sortText": "g",
},
]
`);
});
});
@@ -1,34 +0,0 @@
import { Registry } from '@grafana/data';
import { Monaco, monacoTypes } from '@grafana/ui';
import { PositionContext, SuggestionKind } from '../types';
import { LinkedToken } from '../utils/LinkedToken';
import { toCompletionItem } from '../utils/toCompletionItem';
import { SuggestionsRegistryItem } from './types';
// Given standard and custom registered suggestions and kinds of suggestion expected, return a list of completion items
export const getStandardSuggestions = async (
monaco: Monaco,
currentToken: LinkedToken | null,
suggestionKinds: SuggestionKind[],
positionContext: PositionContext,
suggestionsRegistry: Registry<SuggestionsRegistryItem>
): Promise<monacoTypes.languages.CompletionItem[]> => {
let suggestions: monacoTypes.languages.CompletionItem[] = [];
const invalidRangeToken = currentToken?.isWhiteSpace() || currentToken?.isParenthesis();
const range =
invalidRangeToken || !currentToken?.range
? monaco.Range.fromPositions(positionContext.position)
: currentToken?.range;
// iterating over Set to deduplicate
for (const suggestion of [...new Set(suggestionKinds)]) {
const registeredSuggestions = suggestionsRegistry.getIfExists(suggestion);
if (registeredSuggestions) {
const su = await registeredSuggestions.suggestions({ ...positionContext, range }, monaco);
suggestions = [...suggestions, ...su.map((s) => toCompletionItem(s.label, range, { kind: s.kind, ...s }))];
}
}
return Promise.resolve(suggestions);
};
@@ -1,184 +0,0 @@
import {
multiLineFullQuery,
multiLineFullQueryWithAggregation,
multiLineMultipleColumns,
singleLineEmptyQuery,
singleLineFullQuery,
singleLineFullQueryWithAggregation,
singleLineMultipleColumns,
singleLineTwoQueries,
singleLineTwoQueriesWithAggregation,
} from '../mocks/testData';
import { testStatementPosition } from '../test-utils/statementPosition';
import { StatementPosition } from '../types';
import { initStatementPositionResolvers } from './statementPositionResolversRegistry';
const templateSrvMock = { replace: jest.fn(), getVariables: () => [], getAdhocFilters: jest.fn() };
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
getTemplateSrv: () => templateSrvMock,
}));
describe('statementPosition', () => {
testStatementPosition(
StatementPosition.SelectKeyword,
[
{ query: singleLineEmptyQuery, position: { line: 1, column: 0 } },
{ query: singleLineFullQuery, position: { line: 1, column: 0 } },
{ query: multiLineFullQuery, position: { line: 1, column: 0 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 103 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterSelectKeyword,
[
{ query: singleLineFullQuery, position: { line: 1, column: 7 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 109 } },
{ query: multiLineFullQuery, position: { line: 1, column: 7 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterSelectArguments,
[
{ query: singleLineFullQuery, position: { line: 1, column: 16 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 16 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 118 } },
{ query: multiLineFullQuery, position: { line: 1, column: 16 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterSelectFuncFirstArgument,
[
{ query: singleLineFullQueryWithAggregation, position: { line: 1, column: 14 } },
{ query: multiLineFullQueryWithAggregation, position: { line: 1, column: 14 } },
{ query: singleLineTwoQueriesWithAggregation, position: { line: 1, column: 128 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.FromKeyword,
[
{ query: singleLineFullQuery, position: { line: 1, column: 17 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 119 } },
{ query: multiLineFullQuery, position: { line: 2, column: 0 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterFromKeyword,
[
{ query: singleLineFullQuery, position: { line: 1, column: 21 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 123 } },
{ query: multiLineFullQuery, position: { line: 2, column: 5 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterFrom,
[
{ query: singleLineFullQuery, position: { line: 1, column: 28 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 130 } },
{ query: multiLineFullQuery, position: { line: 2, column: 12 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.WhereKeyword,
[
{ query: singleLineFullQuery, position: { line: 1, column: 34 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 136 } },
{ query: multiLineFullQuery, position: { line: 4, column: 6 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.WhereComparisonOperator,
[
{ query: singleLineFullQuery, position: { line: 1, column: 43 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 145 } },
{ query: multiLineFullQuery, position: { line: 4, column: 15 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.WhereValue,
[
{ query: singleLineFullQuery, position: { line: 1, column: 44 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 146 } },
{ query: multiLineFullQuery, position: { line: 4, column: 16 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterWhereValue,
[
{ query: singleLineFullQuery, position: { line: 1, column: 53 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 155 } },
{ query: multiLineFullQuery, position: { line: 4, column: 25 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterGroupByKeywords,
[
{ query: singleLineFullQuery, position: { line: 1, column: 63 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 167 } },
{ query: multiLineFullQuery, position: { line: 5, column: 11 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterGroupBy,
[
{ query: singleLineFullQuery, position: { line: 1, column: 71 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 173 } },
{ query: multiLineFullQuery, position: { line: 5, column: 18 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterOrderByKeywords,
[
{ query: singleLineFullQuery, position: { line: 1, column: 80 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 181 } },
{ query: multiLineFullQuery, position: { line: 5, column: 26 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterOrderByFunction,
[
{ query: singleLineMultipleColumns, position: { line: 1, column: 108 } },
{ query: multiLineMultipleColumns, position: { line: 5, column: 40 } },
],
initStatementPositionResolvers
);
testStatementPosition(
StatementPosition.AfterOrderByDirection,
[
{ query: singleLineFullQuery, position: { line: 1, column: 92 } },
{ query: singleLineTwoQueries, position: { line: 1, column: 196 } },
{ query: multiLineFullQuery, position: { line: 5, column: 39 } },
],
initStatementPositionResolvers
);
});
@@ -1,31 +0,0 @@
import { Registry } from '@grafana/data';
import { StatementPosition, TokenType } from '../types';
import { LinkedToken } from '../utils/LinkedToken';
import { StatementPositionResolversRegistryItem } from './types';
// Given current cursor position in the SQL editor, returns the statement position.
export function getStatementPosition(
currentToken: LinkedToken | null,
statementPositionResolversRegistry: Registry<StatementPositionResolversRegistryItem>
): StatementPosition[] {
const previousNonWhiteSpace = currentToken?.getPreviousNonWhiteSpaceToken();
const previousKeyword = currentToken?.getPreviousKeyword();
const previousIsSlash = currentToken?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Operator, '/');
const resolvers = statementPositionResolversRegistry.list();
const positions = [];
for (const resolver of resolvers) {
if (
resolver.resolve(currentToken, previousKeyword ?? null, previousNonWhiteSpace ?? null, Boolean(previousIsSlash))
) {
positions.push(resolver.id);
}
}
if (positions.length === 0) {
return [StatementPosition.Unknown];
}
return positions;
}
@@ -1,880 +0,0 @@
import { monacoTypes } from '@grafana/ui';
import { SQLMonarchLanguage } from './types';
// STD basic SQL
export const SELECT = 'select';
export const FROM = 'from';
export const WHERE = 'where';
export const GROUP = 'group';
export const ORDER = 'order';
export const BY = 'by';
export const DESC = 'desc';
export const ASC = 'asc';
export const LIMIT = 'limit';
export const WITH = 'with';
export const AS = 'as';
export const SCHEMA = 'schema';
export const STD_STATS = ['AVG', 'COUNT', 'MAX', 'MIN', 'SUM'];
export const AND = 'AND';
export const OR = 'OR';
export const LOGICAL_OPERATORS = [AND, OR];
export const EQUALS = '=';
export const NOT_EQUALS = '!=';
export const COMPARISON_OPERATORS = [EQUALS, NOT_EQUALS];
export const STD_OPERATORS = [...COMPARISON_OPERATORS];
export const conf: monacoTypes.languages.LanguageConfiguration = {
comments: {
lineComment: '--',
blockComment: ['/*', '*/'],
},
brackets: [
['{', '}'],
['[', ']'],
['(', ')'],
],
autoClosingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
surroundingPairs: [
{ open: '{', close: '}' },
{ open: '[', close: ']' },
{ open: '(', close: ')' },
{ open: '"', close: '"' },
{ open: "'", close: "'" },
],
};
// based on https://github.com/microsoft/monaco-editor/blob/main/src/basic-languages/sql/sql.ts
export const language: SQLMonarchLanguage = {
defaultToken: '',
tokenPostfix: '.sql',
ignoreCase: true,
brackets: [
{ open: '[', close: ']', token: 'delimiter.square' },
{ open: '(', close: ')', token: 'delimiter.parenthesis' },
],
keywords: [
'ABORT',
'ABSOLUTE',
'ACTION',
'ADA',
'ADD',
'AFTER',
'ALL',
'ALLOCATE',
'ALTER',
'ALWAYS',
'ANALYZE',
'AND',
'ANY',
'ARE',
'AS',
'ASC',
'ASSERTION',
'AT',
'ATTACH',
'AUTHORIZATION',
'AUTOINCREMENT',
'AVG',
'BACKUP',
'BEFORE',
'BEGIN',
'BETWEEN',
'BIT',
'BIT_LENGTH',
'BOTH',
'BREAK',
'BROWSE',
'BULK',
'BY',
'CASCADE',
'CASCADED',
'CASE',
'CAST',
'CATALOG',
'CHAR',
'CHARACTER',
'CHARACTER_LENGTH',
'CHAR_LENGTH',
'CHECK',
'CHECKPOINT',
'CLOSE',
'CLUSTERED',
'COALESCE',
'COLLATE',
'COLLATION',
'COLUMN',
'COMMIT',
'COMPUTE',
'CONFLICT',
'CONNECT',
'CONNECTION',
'CONSTRAINT',
'CONSTRAINTS',
'CONTAINS',
'CONTAINSTABLE',
'CONTINUE',
'CONVERT',
'CORRESPONDING',
'COUNT',
'CREATE',
'CROSS',
'CURRENT',
'CURRENT_DATE',
'CURRENT_TIME',
'CURRENT_TIMESTAMP',
'CURRENT_USER',
'CURSOR',
'DATABASE',
'DATE',
'DAY',
'DBCC',
'DEALLOCATE',
'DEC',
'DECIMAL',
'DECLARE',
'DEFAULT',
'DEFERRABLE',
'DEFERRED',
'DELETE',
'DENY',
'DESC',
'DESCRIBE',
'DESCRIPTOR',
'DETACH',
'DIAGNOSTICS',
'DISCONNECT',
'DISK',
'DISTINCT',
'DISTRIBUTED',
'DO',
'DOMAIN',
'DOUBLE',
'DROP',
'DUMP',
'EACH',
'ELSE',
'END',
'END-EXEC',
'ERRLVL',
'ESCAPE',
'EXCEPT',
'EXCEPTION',
'EXCLUDE',
'EXCLUSIVE',
'EXEC',
'EXECUTE',
'EXISTS',
'EXIT',
'EXPLAIN',
'EXTERNAL',
'EXTRACT',
'FAIL',
'FALSE',
'FETCH',
'FILE',
'FILLFACTOR',
'FILTER',
'FIRST',
'FLOAT',
'FOLLOWING',
'FOR',
'FOREIGN',
'FORTRAN',
'FOUND',
'FREETEXT',
'FREETEXTTABLE',
'FROM',
'FULL',
'FUNCTION',
'GENERATED',
'GET',
'GLOB',
'GLOBAL',
'GO',
'GOTO',
'GRANT',
'GROUP',
'GROUPS',
'HAVING',
'HOLDLOCK',
'HOUR',
'IDENTITY',
'IDENTITYCOL',
'IDENTITY_INSERT',
'IF',
'IGNORE',
'IMMEDIATE',
'IN',
'INCLUDE',
'INDEX',
'INDEXED',
'INDICATOR',
'INITIALLY',
'INNER',
'INPUT',
'INSENSITIVE',
'INSERT',
'INSTEAD',
'INT',
'INTEGER',
'INTERSECT',
'INTERVAL',
'INTO',
'IS',
'ISNULL',
'ISOLATION',
'JOIN',
'KEY',
'KILL',
'LANGUAGE',
'LAST',
'LEADING',
'LEFT',
'LEVEL',
'LIKE',
'LIMIT',
'LINENO',
'LOAD',
'LOCAL',
'LOWER',
'MATCH',
'MATERIALIZED',
'MAX',
'MERGE',
'MIN',
'MINUTE',
'MODULE',
'MONTH',
'NAMES',
'NATIONAL',
'NATURAL',
'NCHAR',
'NEXT',
'NO',
'NOCHECK',
'NONCLUSTERED',
'NONE',
'NOT',
'NOTHING',
'NOTNULL',
'NULL',
'NULLIF',
'NULLS',
'NUMERIC',
'OCTET_LENGTH',
'OF',
'OFF',
'OFFSET',
'OFFSETS',
'ON',
'ONLY',
'OPEN',
'OPENDATASOURCE',
'OPENQUERY',
'OPENROWSET',
'OPENXML',
'OPTION',
'OR',
'ORDER',
'OTHERS',
'OUTER',
'OUTPUT',
'OVER',
'OVERLAPS',
'PAD',
'PARTIAL',
'PARTITION',
'PASCAL',
'PERCENT',
'PIVOT',
'PLAN',
'POSITION',
'PRAGMA',
'PRECEDING',
'PRECISION',
'PREPARE',
'PRESERVE',
'PRIMARY',
'PRINT',
'PRIOR',
'PRIVILEGES',
'PROC',
'PROCEDURE',
'PUBLIC',
'QUERY',
'RAISE',
'RAISERROR',
'RANGE',
'READ',
'READTEXT',
'REAL',
'RECONFIGURE',
'RECURSIVE',
'REFERENCES',
'REGEXP',
'REINDEX',
'RELATIVE',
'RELEASE',
'RENAME',
'REPLACE',
'REPLICATION',
'RESTORE',
'RESTRICT',
'RETURN',
'RETURNING',
'REVERT',
'REVOKE',
'RIGHT',
'ROLLBACK',
'ROW',
'ROWCOUNT',
'ROWGUIDCOL',
'ROWS',
'RULE',
'SAVE',
'SAVEPOINT',
'SCHEMA',
'SCROLL',
'SECOND',
'SECTION',
'SECURITYAUDIT',
'SELECT',
'SEMANTICKEYPHRASETABLE',
'SEMANTICSIMILARITYDETAILSTABLE',
'SEMANTICSIMILARITYTABLE',
'SESSION',
'SESSION_USER',
'SET',
'SETUSER',
'SHUTDOWN',
'SIZE',
'SMALLINT',
'SOME',
'SPACE',
'SQL',
'SQLCA',
'SQLCODE',
'SQLERROR',
'SQLSTATE',
'SQLWARNING',
'STATISTICS',
'SUBSTRING',
'SUM',
'SYSTEM_USER',
'TABLE',
'TABLESAMPLE',
'TEMP',
'TEMPORARY',
'TEXTSIZE',
'THEN',
'TIES',
'TIME',
'TIMESTAMP',
'TIMEZONE_HOUR',
'TIMEZONE_MINUTE',
'TO',
'TOP',
'TRAILING',
'TRAN',
'TRANSACTION',
'TRANSLATE',
'TRANSLATION',
'TRIGGER',
'TRIM',
'TRUE',
'TRUNCATE',
'TRY_CONVERT',
'TSEQUAL',
'UNBOUNDED',
'UNION',
'UNIQUE',
'UNKNOWN',
'UNPIVOT',
'UPDATE',
'UPDATETEXT',
'UPPER',
'USAGE',
'USE',
'USER',
'USING',
'VACUUM',
'VALUE',
'VALUES',
'VARCHAR',
'VARYING',
'VIEW',
'VIRTUAL',
'WAITFOR',
'WHEN',
'WHENEVER',
'WHERE',
'WHILE',
'WINDOW',
'WITH',
'WITHIN GROUP',
'WITHOUT',
'WORK',
'WRITE',
'WRITETEXT',
'YEAR',
'ZONE',
],
operators: [
// Set
'EXCEPT',
'INTERSECT',
'UNION',
// Join
'APPLY',
'CROSS',
'FULL',
'INNER',
'JOIN',
'LEFT',
'OUTER',
'RIGHT',
// Predicates
'CONTAINS',
'FREETEXT',
'IS',
'NULL',
// Pivoting
'PIVOT',
'UNPIVOT',
// Merging
'MATCHED',
],
logicalOperators: ['ALL', 'AND', 'ANY', 'BETWEEN', 'EXISTS', 'IN', 'LIKE', 'NOT', 'OR', 'SOME'],
comparisonOperators: ['<>', '>', '<', '>=', '<=', '=', '!=', '&', '~', '^', '%'],
builtinFunctions: [
// Aggregate
'AVG',
'CHECKSUM_AGG',
'COUNT',
'COUNT_BIG',
'GROUPING',
'GROUPING_ID',
'MAX',
'MIN',
'SUM',
'STDEV',
'STDEVP',
'VAR',
'VARP',
// Analytic
'CUME_DIST',
'FIRST_VALUE',
'LAG',
'LAST_VALUE',
'LEAD',
'PERCENTILE_CONT',
'PERCENTILE_DISC',
'PERCENT_RANK',
// Collation
'COLLATE',
'COLLATIONPROPERTY',
'TERTIARY_WEIGHTS',
// Azure
'FEDERATION_FILTERING_VALUE',
// Conversion
'CAST',
'CONVERT',
'PARSE',
'TRY_CAST',
'TRY_CONVERT',
'TRY_PARSE',
// Cryptographic
'ASYMKEY_ID',
'ASYMKEYPROPERTY',
'CERTPROPERTY',
'CERT_ID',
'CRYPT_GEN_RANDOM',
'DECRYPTBYASYMKEY',
'DECRYPTBYCERT',
'DECRYPTBYKEY',
'DECRYPTBYKEYAUTOASYMKEY',
'DECRYPTBYKEYAUTOCERT',
'DECRYPTBYPASSPHRASE',
'ENCRYPTBYASYMKEY',
'ENCRYPTBYCERT',
'ENCRYPTBYKEY',
'ENCRYPTBYPASSPHRASE',
'HASHBYTES',
'IS_OBJECTSIGNED',
'KEY_GUID',
'KEY_ID',
'KEY_NAME',
'SIGNBYASYMKEY',
'SIGNBYCERT',
'SYMKEYPROPERTY',
'VERIFYSIGNEDBYCERT',
'VERIFYSIGNEDBYASYMKEY',
// Cursor
'CURSOR_STATUS',
// Datatype
'DATALENGTH',
'IDENT_CURRENT',
'IDENT_INCR',
'IDENT_SEED',
'IDENTITY',
'SQL_VARIANT_PROPERTY',
// Datetime
'CURRENT_TIMESTAMP',
'DATEADD',
'DATEDIFF',
'DATEFROMPARTS',
'DATENAME',
'DATEPART',
'DATETIME2FROMPARTS',
'DATETIMEFROMPARTS',
'DATETIMEOFFSETFROMPARTS',
'DAY',
'EOMONTH',
'GETDATE',
'GETUTCDATE',
'ISDATE',
'MONTH',
'SMALLDATETIMEFROMPARTS',
'SWITCHOFFSET',
'SYSDATETIME',
'SYSDATETIMEOFFSET',
'SYSUTCDATETIME',
'TIMEFROMPARTS',
'TODATETIMEOFFSET',
'YEAR',
// Logical
'CHOOSE',
'COALESCE',
'IIF',
'NULLIF',
// Mathematical
'ABS',
'ACOS',
'ASIN',
'ATAN',
'ATN2',
'CEILING',
'COS',
'COT',
'DEGREES',
'EXP',
'FLOOR',
'LOG',
'LOG10',
'PI',
'POWER',
'RADIANS',
'RAND',
'ROUND',
'SIGN',
'SIN',
'SQRT',
'SQUARE',
'TAN',
// Metadata
'APP_NAME',
'APPLOCK_MODE',
'APPLOCK_TEST',
'ASSEMBLYPROPERTY',
'COL_LENGTH',
'COL_NAME',
'COLUMNPROPERTY',
'DATABASE_PRINCIPAL_ID',
'DATABASEPROPERTYEX',
'DB_ID',
'DB_NAME',
'FILE_ID',
'FILE_IDEX',
'FILE_NAME',
'FILEGROUP_ID',
'FILEGROUP_NAME',
'FILEGROUPPROPERTY',
'FILEPROPERTY',
'FULLTEXTCATALOGPROPERTY',
'FULLTEXTSERVICEPROPERTY',
'INDEX_COL',
'INDEXKEY_PROPERTY',
'INDEXPROPERTY',
'OBJECT_DEFINITION',
'OBJECT_ID',
'OBJECT_NAME',
'OBJECT_SCHEMA_NAME',
'OBJECTPROPERTY',
'OBJECTPROPERTYEX',
'ORIGINAL_DB_NAME',
'PARSENAME',
'SCHEMA_ID',
'SCHEMA_NAME',
'SCOPE_IDENTITY',
'SERVERPROPERTY',
'STATS_DATE',
'TYPE_ID',
'TYPE_NAME',
'TYPEPROPERTY',
// Ranking
'DENSE_RANK',
'NTILE',
'RANK',
'ROW_NUMBER',
// Replication
'PUBLISHINGSERVERNAME',
// Rowset
'OPENDATASOURCE',
'OPENQUERY',
'OPENROWSET',
'OPENXML',
// Security
'CERTENCODED',
'CERTPRIVATEKEY',
'CURRENT_USER',
'HAS_DBACCESS',
'HAS_PERMS_BY_NAME',
'IS_MEMBER',
'IS_ROLEMEMBER',
'IS_SRVROLEMEMBER',
'LOGINPROPERTY',
'ORIGINAL_LOGIN',
'PERMISSIONS',
'PWDENCRYPT',
'PWDCOMPARE',
'SESSION_USER',
'SESSIONPROPERTY',
'SUSER_ID',
'SUSER_NAME',
'SUSER_SID',
'SUSER_SNAME',
'SYSTEM_USER',
'USER',
'USER_ID',
'USER_NAME',
// String
'ASCII',
'CHAR',
'CHARINDEX',
'CONCAT',
'DIFFERENCE',
'FORMAT',
'LEFT',
'LEN',
'LOWER',
'LTRIM',
'NCHAR',
'PATINDEX',
'QUOTENAME',
'REPLACE',
'REPLICATE',
'REVERSE',
'RIGHT',
'RTRIM',
'SOUNDEX',
'SPACE',
'STR',
'STUFF',
'SUBSTRING',
'UNICODE',
'UPPER',
// System
'BINARY_CHECKSUM',
'CHECKSUM',
'CONNECTIONPROPERTY',
'CONTEXT_INFO',
'CURRENT_REQUEST_ID',
'ERROR_LINE',
'ERROR_NUMBER',
'ERROR_MESSAGE',
'ERROR_PROCEDURE',
'ERROR_SEVERITY',
'ERROR_STATE',
'FORMATMESSAGE',
'GETANSINULL',
'GET_FILESTREAM_TRANSACTION_CONTEXT',
'HOST_ID',
'HOST_NAME',
'ISNULL',
'ISNUMERIC',
'MIN_ACTIVE_ROWVERSION',
'NEWID',
'NEWSEQUENTIALID',
'ROWCOUNT_BIG',
'XACT_STATE',
// TextImage
'TEXTPTR',
'TEXTVALID',
// Trigger
'COLUMNS_UPDATED',
'EVENTDATA',
'TRIGGER_NESTLEVEL',
'UPDATE',
// ChangeTracking
'CHANGETABLE',
'CHANGE_TRACKING_CONTEXT',
'CHANGE_TRACKING_CURRENT_VERSION',
'CHANGE_TRACKING_IS_COLUMN_IN_MASK',
'CHANGE_TRACKING_MIN_VALID_VERSION',
// FullTextSearch
'CONTAINSTABLE',
'FREETEXTTABLE',
// SemanticTextSearch
'SEMANTICKEYPHRASETABLE',
'SEMANTICSIMILARITYDETAILSTABLE',
'SEMANTICSIMILARITYTABLE',
// FileStream
'FILETABLEROOTPATH',
'GETFILENAMESPACEPATH',
'GETPATHLOCATOR',
'PATHNAME',
// ServiceBroker
'GET_TRANSMISSION_STATUS',
],
builtinVariables: [
// Configuration
'@@DATEFIRST',
'@@DBTS',
'@@LANGID',
'@@LANGUAGE',
'@@LOCK_TIMEOUT',
'@@MAX_CONNECTIONS',
'@@MAX_PRECISION',
'@@NESTLEVEL',
'@@OPTIONS',
'@@REMSERVER',
'@@SERVERNAME',
'@@SERVICENAME',
'@@SPID',
'@@TEXTSIZE',
'@@VERSION',
// Cursor
'@@CURSOR_ROWS',
'@@FETCH_STATUS',
// Datetime
'@@DATEFIRST',
// Metadata
'@@PROCID',
// System
'@@ERROR',
'@@IDENTITY',
'@@ROWCOUNT',
'@@TRANCOUNT',
// Stats
'@@CONNECTIONS',
'@@CPU_BUSY',
'@@IDLE',
'@@IO_BUSY',
'@@PACKET_ERRORS',
'@@PACK_RECEIVED',
'@@PACK_SENT',
'@@TIMETICKS',
'@@TOTAL_ERRORS',
'@@TOTAL_READ',
'@@TOTAL_WRITE',
],
pseudoColumns: ['$ACTION', '$IDENTITY', '$ROWGUID', '$PARTITION'],
tokenizer: {
root: [
{ include: '@templateVariables' },
{ include: '@macros' },
{ include: '@comments' },
{ include: '@whitespace' },
{ include: '@pseudoColumns' },
{ include: '@numbers' },
{ include: '@strings' },
{ include: '@complexIdentifiers' },
{ include: '@scopes' },
[/[;,.]/, 'delimiter'],
[/[()]/, '@brackets'],
[
/[\w@#$|<|>|=|!|%|&|+|\|-|*|/|~|^]+/,
{
cases: {
'@operators': 'operator',
'@comparisonOperators': 'operator',
'@logicalOperators': 'operator',
'@builtinVariables': 'predefined',
'@builtinFunctions': 'predefined',
'@keywords': 'keyword',
'@default': 'identifier',
},
},
],
],
templateVariables: [[/\$[a-zA-Z0-9]+/, 'variable']],
macros: [[/\$__[a-zA-Z0-9-_]+/, 'type']],
whitespace: [[/\s+/, 'white']],
comments: [
[/--+.*/, 'comment'],
[/\/\*/, { token: 'comment.quote', next: '@comment' }],
],
comment: [
[/[^*/]+/, 'comment'],
// Not supporting nested comments, as nested comments seem to not be standard?
// i.e. http://stackoverflow.com/questions/728172/are-there-multiline-comment-delimiters-in-sql-that-are-vendor-agnostic
// [/\/\*/, { token: 'comment.quote', next: '@push' }], // nested comment not allowed :-(
[/\*\//, { token: 'comment.quote', next: '@pop' }],
[/./, 'comment'],
],
pseudoColumns: [
[
/[$][A-Za-z_][\w@#$]*/,
{
cases: {
'@pseudoColumns': 'predefined',
'@default': 'identifier',
},
},
],
],
numbers: [
[/0[xX][0-9a-fA-F]*/, 'number'],
[/[$][+-]*\d*(\.\d*)?/, 'number'],
[/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, 'number'],
],
strings: [
[/N'/, { token: 'string', next: '@string' }],
[/'/, { token: 'string', next: '@string' }],
],
string: [
[/[^']+/, 'string'],
[/''/, 'string'],
[/'/, { token: 'string', next: '@pop' }],
],
complexIdentifiers: [
[/\[/, { token: 'identifier.quote', next: '@bracketedIdentifier' }],
[/"/, { token: 'identifier.quote', next: '@quotedIdentifier' }],
],
bracketedIdentifier: [
[/[^\]]+/, 'identifier'],
[/]]/, 'identifier'],
[/]/, { token: 'identifier.quote', next: '@pop' }],
],
quotedIdentifier: [
[/[^"]+/, 'identifier'],
[/""/, 'identifier'],
[/"/, { token: 'identifier.quote', next: '@pop' }],
],
scopes: [
[/BEGIN\s+(DISTRIBUTED\s+)?TRAN(SACTION)?\b/i, 'keyword'],
[/BEGIN\s+TRY\b/i, { token: 'keyword.try' }],
[/END\s+TRY\b/i, { token: 'keyword.try' }],
[/BEGIN\s+CATCH\b/i, { token: 'keyword.catch' }],
[/END\s+CATCH\b/i, { token: 'keyword.catch' }],
[/(BEGIN|CASE)\b/i, { token: 'keyword.block' }],
[/END\b/i, { token: 'keyword.block' }],
[/WHEN\b/i, { token: 'keyword.choice' }],
[/THEN\b/i, { token: 'keyword.choice' }],
],
},
};
@@ -1,67 +0,0 @@
import { MacrosRegistryItem } from './types';
const COLUMN = 'column',
RELATIVE_TIME_STRING = "'5m'";
export enum MacroType {
Value,
Filter,
Group,
Column,
Table,
}
export const MACROS: MacrosRegistryItem[] = [
{
id: '$__timeFilter(dateColumn)',
name: '$__timeFilter(dateColumn)',
text: '$__timeFilter',
args: [COLUMN],
type: MacroType.Filter,
description:
'Will be replaced by a time range filter using the specified column name. For example, dateColumn BETWEEN FROM_UNIXTIME(1494410783) AND FROM_UNIXTIME(1494410983)',
},
{
id: '$__timeFrom()',
name: '$__timeFrom()',
text: '$__timeFrom',
args: [],
type: MacroType.Filter,
description:
'Will be replaced by the start of the currently active time selection. For example, FROM_UNIXTIME(1494410783)',
},
{
id: '$__timeTo()',
name: '$__timeTo()',
text: '$__timeTo',
args: [],
type: MacroType.Filter,
description:
'Will be replaced by the end of the currently active time selection. For example, FROM_UNIXTIME(1494410983)',
},
{
id: "$__timeGroup(dateColumn, '5m')",
name: "$__timeGroup(dateColumn, '5m')",
text: '$__timeGroup',
args: [COLUMN, RELATIVE_TIME_STRING],
type: MacroType.Value,
description:
'Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),*',
},
{
id: '$__table',
name: '$__table',
text: '$__table',
args: [],
type: MacroType.Table,
description: 'Will be replaced by the query table.',
},
{
id: '$__column',
name: '$__column',
text: '$__column',
args: [],
type: MacroType.Column,
description: 'Will be replaced by the query column.',
},
];
@@ -1,425 +0,0 @@
import { Registry } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import {
CompletionItemInsertTextRule,
CompletionItemKind,
CompletionItemPriority,
MacroType,
OperatorType,
SuggestionKind,
} from '../types';
import { TRIGGER_SUGGEST } from '../utils/commands';
import { ASC, DESC, LOGICAL_OPERATORS, STD_OPERATORS, STD_STATS } from './language';
import { MACROS } from './macros';
import { FunctionsRegistryItem, MacrosRegistryItem, OperatorsRegistryItem, SuggestionsRegistryItem } from './types';
/**
* This registry glues particular SuggestionKind with an async function that provides completion items for it.
* To add a new suggestion kind, SQLEditor should be configured with a provider that implements customSuggestionKinds.
*/
export const initStandardSuggestions =
(
functions: Registry<FunctionsRegistryItem>,
operators: Registry<OperatorsRegistryItem>,
macros: Registry<MacrosRegistryItem>
) =>
(): SuggestionsRegistryItem[] =>
[
{
id: SuggestionKind.SelectKeyword,
name: SuggestionKind.SelectKeyword,
suggestions: (_, m) =>
Promise.resolve([
{
label: `SELECT <column>`,
insertText: `SELECT $0`,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
kind: CompletionItemKind.Snippet,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.Medium,
},
{
label: `SELECT <column> FROM <table>`,
insertText: `SELECT $2 FROM $1`,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
kind: CompletionItemKind.Snippet,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.Medium,
},
]),
},
{
id: SuggestionKind.TemplateVariables,
name: SuggestionKind.TemplateVariables,
suggestions: (_, m) => {
const templateSrv = getTemplateSrv();
if (!templateSrv) {
return Promise.resolve([]);
}
return Promise.resolve(
templateSrv.getVariables().map((variable) => {
const label = `\$${variable.name}`;
const val = templateSrv.replace(label);
return {
label,
detail: `(Template Variable) ${val}`,
kind: CompletionItemKind.Snippet,
documentation: `(Template Variable) ${val}`,
insertText: `\\$${variable.name} `,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
command: TRIGGER_SUGGEST,
};
})
);
},
},
{
id: SuggestionKind.SelectMacro,
name: SuggestionKind.SelectMacro,
suggestions: (_, m) =>
Promise.resolve([
...macros
.list()
.filter((m) => m.type === MacroType.Value || m.type === MacroType.Column)
.map(createMacroSuggestionItem),
]),
},
{
id: SuggestionKind.TableMacro,
name: SuggestionKind.TableMacro,
suggestions: (_, m) =>
Promise.resolve([
...macros
.list()
.filter((m) => m.type === MacroType.Table)
.map(createMacroSuggestionItem),
]),
},
{
id: SuggestionKind.GroupMacro,
name: SuggestionKind.GroupMacro,
suggestions: (_, m) =>
Promise.resolve([
...macros
.list()
.filter((m) => m.type === MacroType.Group)
.map(createMacroSuggestionItem),
]),
},
{
id: SuggestionKind.FilterMacro,
name: SuggestionKind.FilterMacro,
suggestions: (_, m) =>
Promise.resolve([
...macros
.list()
.filter((m) => m.type === MacroType.Filter)
.map(createMacroSuggestionItem),
]),
},
{
id: SuggestionKind.WithKeyword,
name: SuggestionKind.WithKeyword,
suggestions: (_, m) =>
Promise.resolve([
{
label: `WITH <alias> AS ( ... )`,
insertText: `WITH $1 AS ( $2 )`,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
kind: CompletionItemKind.Snippet,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.Medium,
},
]),
},
{
id: SuggestionKind.FunctionsWithArguments,
name: SuggestionKind.FunctionsWithArguments,
suggestions: (_, m) =>
Promise.resolve([
...functions.list().map((f) => ({
label: f.name,
insertText: `${f.name}($0)`,
documentation: f.description,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
kind: CompletionItemKind.Function,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumHigh,
})),
]),
},
{
id: SuggestionKind.FunctionsWithoutArguments,
name: SuggestionKind.FunctionsWithoutArguments,
suggestions: (_, m) =>
Promise.resolve([
...functions.list().map((f) => ({
label: f.name,
insertText: `${f.name}()`,
documentation: f.description,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
kind: CompletionItemKind.Function,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumHigh,
})),
]),
},
{
id: SuggestionKind.FromKeyword,
name: SuggestionKind.FromKeyword,
suggestions: (_, m) =>
Promise.resolve([
{
label: 'FROM',
insertText: `FROM $0`,
command: TRIGGER_SUGGEST,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
kind: CompletionItemKind.Keyword,
},
]),
},
{
id: SuggestionKind.Tables,
name: SuggestionKind.Tables,
suggestions: (_, m) => Promise.resolve([]),
},
{
id: SuggestionKind.Columns,
name: SuggestionKind.Columns,
suggestions: (_, m) => Promise.resolve([]),
},
{
id: SuggestionKind.LogicalOperators,
name: SuggestionKind.LogicalOperators,
suggestions: (_, m) =>
Promise.resolve(
operators
.list()
.filter((o) => o.type === OperatorType.Logical)
.map((o) => ({
label: o.operator,
insertText: `${o.operator} `,
documentation: o.description,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumHigh,
kind: CompletionItemKind.Operator,
}))
),
},
{
id: SuggestionKind.WhereKeyword,
name: SuggestionKind.WhereKeyword,
suggestions: (_, m) =>
Promise.resolve([
{
label: 'WHERE',
insertText: `WHERE `,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumHigh,
kind: CompletionItemKind.Keyword,
},
]),
},
{
id: SuggestionKind.ComparisonOperators,
name: SuggestionKind.ComparisonOperators,
suggestions: (_, m) =>
Promise.resolve([
...operators
.list()
.filter((o) => o.type === OperatorType.Comparison)
.map((o) => ({
label: o.operator,
insertText: `${o.operator} `,
documentation: o.description,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumHigh,
kind: CompletionItemKind.Operator,
})),
{
label: 'IN (...)',
insertText: `IN ( $0 )`,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.Medium,
kind: CompletionItemKind.Operator,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: 'NOT IN (...)',
insertText: `NOT IN ( $0 )`,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.Medium,
kind: CompletionItemKind.Operator,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: 'IS',
insertText: `IS`,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.Medium,
kind: CompletionItemKind.Operator,
},
{
label: 'IS NOT',
insertText: `IS NOT`,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.Medium,
kind: CompletionItemKind.Operator,
},
]),
},
{
id: SuggestionKind.GroupByKeywords,
name: SuggestionKind.GroupByKeywords,
suggestions: (_, m) =>
Promise.resolve([
{
label: 'GROUP BY',
insertText: `GROUP BY `,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumHigh,
kind: CompletionItemKind.Keyword,
},
]),
},
{
id: SuggestionKind.OrderByKeywords,
name: SuggestionKind.OrderByKeywords,
suggestions: (_, m) =>
Promise.resolve([
{
label: 'ORDER BY',
insertText: `ORDER BY `,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.Medium,
kind: CompletionItemKind.Keyword,
},
{
label: 'ORDER BY(ascending)',
insertText: `ORDER BY $1 ASC `,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumLow,
kind: CompletionItemKind.Snippet,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
},
{
label: 'ORDER BY(descending)',
insertText: `ORDER BY $1 DESC`,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumLow,
kind: CompletionItemKind.Snippet,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
},
]),
},
{
id: SuggestionKind.LimitKeyword,
name: SuggestionKind.LimitKeyword,
suggestions: (_, m) =>
Promise.resolve([
{
label: 'LIMIT',
insertText: `LIMIT `,
command: TRIGGER_SUGGEST,
sortText: CompletionItemPriority.MediumLow,
kind: CompletionItemKind.Keyword,
},
]),
},
{
id: SuggestionKind.SortOrderDirectionKeyword,
name: SuggestionKind.SortOrderDirectionKeyword,
suggestions: (_, m) =>
Promise.resolve(
[ASC, DESC].map((o) => ({
label: o,
insertText: `${o} `,
command: TRIGGER_SUGGEST,
kind: CompletionItemKind.Keyword,
}))
),
},
{
id: SuggestionKind.NotKeyword,
name: SuggestionKind.NotKeyword,
suggestions: () =>
Promise.resolve([
{
label: 'NOT',
insertText: 'NOT',
command: TRIGGER_SUGGEST,
kind: CompletionItemKind.Keyword,
sortText: CompletionItemPriority.High,
},
]),
},
{
id: SuggestionKind.BoolValues,
name: SuggestionKind.BoolValues,
suggestions: () =>
Promise.resolve(
['TRUE', 'FALSE'].map((o) => ({
label: o,
insertText: `${o}`,
command: TRIGGER_SUGGEST,
kind: CompletionItemKind.Keyword,
sortText: CompletionItemPriority.Medium,
}))
),
},
{
id: SuggestionKind.NullValue,
name: SuggestionKind.NullValue,
suggestions: () =>
Promise.resolve(
['NULL'].map((o) => ({
label: o,
insertText: `${o}`,
command: TRIGGER_SUGGEST,
kind: CompletionItemKind.Keyword,
sortText: CompletionItemPriority.Low,
}))
),
},
];
export const initFunctionsRegistry = (): FunctionsRegistryItem[] => [
...STD_STATS.map((s) => ({
id: s,
name: s,
})),
];
export const initMacrosRegistry = (): MacrosRegistryItem[] => [...MACROS];
export const initOperatorsRegistry = (): OperatorsRegistryItem[] => [
...STD_OPERATORS.map((o) => ({
id: o,
name: o,
operator: o,
type: OperatorType.Comparison,
})),
...LOGICAL_OPERATORS.map((o) => ({ id: o, name: o.toUpperCase(), operator: o, type: OperatorType.Logical })),
];
function createMacroSuggestionItem(m: MacrosRegistryItem) {
return {
label: m.name,
insertText: `${'\\' + m.text}${argsString(m.args)} `,
insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet,
kind: CompletionItemKind.Snippet,
documentation: m.description,
command: TRIGGER_SUGGEST,
};
}
function argsString(args?: string[]): string {
if (!args) {
return '()';
}
return '('.concat(args.map((t, i) => `\${${i}:${t}}`).join(', ')).concat(')');
}
@@ -1,244 +0,0 @@
import { StatementPosition, TokenType } from '../types';
import { AND, AS, ASC, BY, DESC, FROM, GROUP, ORDER, SELECT, WHERE, WITH } from './language';
import { StatementPositionResolversRegistryItem } from './types';
export function initStatementPositionResolvers(): StatementPositionResolversRegistryItem[] {
return [
{
id: StatementPosition.SelectKeyword,
name: StatementPosition.SelectKeyword,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
currentToken === null ||
(currentToken.isWhiteSpace() && currentToken.previous === null) ||
currentToken.is(TokenType.Keyword, SELECT) ||
(currentToken.is(TokenType.Keyword, SELECT) && currentToken.previous === null) ||
previousIsSlash ||
(currentToken.isIdentifier() && (previousIsSlash || currentToken?.previous === null)) ||
(currentToken.isIdentifier() && SELECT.startsWith(currentToken.value.toLowerCase()))
),
},
{
id: StatementPosition.WithKeyword,
name: StatementPosition.WithKeyword,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
currentToken === null ||
(currentToken.isWhiteSpace() && currentToken.previous === null) ||
(currentToken.is(TokenType.Keyword, WITH) && currentToken.previous === null) ||
(currentToken.isIdentifier() && WITH.toLowerCase().startsWith(currentToken.value.toLowerCase()))
),
},
{
id: StatementPosition.AfterSelectKeyword,
name: StatementPosition.AfterSelectKeyword,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(previousNonWhiteSpace?.value.toLowerCase() === SELECT),
},
{
id: StatementPosition.AfterSelectArguments,
name: StatementPosition.AfterSelectArguments,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(previousKeyword?.value.toLowerCase() === SELECT && previousNonWhiteSpace?.value === ',');
},
},
{
id: StatementPosition.AfterSelectFuncFirstArgument,
name: StatementPosition.AfterSelectFuncFirstArgument,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(
(previousKeyword?.value.toLowerCase() === SELECT || previousKeyword?.value.toLowerCase() === AS) &&
(previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') || currentToken?.is(TokenType.Parenthesis, '()'))
);
},
},
{
id: StatementPosition.AfterWhereFunctionArgument,
name: StatementPosition.AfterWhereFunctionArgument,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(
previousKeyword?.is(TokenType.Keyword, WHERE) &&
(previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') || currentToken?.is(TokenType.Parenthesis, '()'))
);
},
},
{
id: StatementPosition.AfterGroupBy,
name: StatementPosition.AfterGroupBy,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
previousKeyword?.is(TokenType.Keyword, BY) &&
previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, GROUP) &&
(previousNonWhiteSpace?.isIdentifier() ||
previousNonWhiteSpace?.isDoubleQuotedString() ||
previousNonWhiteSpace?.is(TokenType.Parenthesis, ')') ||
previousNonWhiteSpace?.is(TokenType.Parenthesis, '()'))
),
},
{
id: StatementPosition.SelectAlias,
name: StatementPosition.SelectAlias,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
if (previousNonWhiteSpace?.value === ',' && previousKeyword?.value.toLowerCase() === AS) {
return true;
}
return false;
},
},
{
id: StatementPosition.FromKeyword,
name: StatementPosition.FromKeyword,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(
(previousKeyword?.value.toLowerCase() === SELECT && previousNonWhiteSpace?.value !== ',') ||
((currentToken?.isKeyword() || currentToken?.isIdentifier()) &&
FROM.toLowerCase().startsWith(currentToken.value.toLowerCase()))
);
},
},
{
id: StatementPosition.AfterFromKeyword,
name: StatementPosition.AfterFromKeyword,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(previousNonWhiteSpace?.value.toLowerCase() === FROM),
},
{
id: StatementPosition.AfterFrom,
name: StatementPosition.AfterFrom,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
(previousKeyword?.value.toLowerCase() === FROM && previousNonWhiteSpace?.isDoubleQuotedString()) ||
(previousKeyword?.value.toLowerCase() === FROM && previousNonWhiteSpace?.isIdentifier()) ||
(previousKeyword?.value.toLowerCase() === FROM && previousNonWhiteSpace?.isVariable())
),
},
{
id: StatementPosition.AfterTable,
name: StatementPosition.AfterTable,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(
previousKeyword?.value.toLowerCase() === FROM &&
(previousNonWhiteSpace?.isVariable() || previousNonWhiteSpace?.value !== '')
);
},
},
{
id: StatementPosition.WhereKeyword,
name: StatementPosition.WhereKeyword,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
previousKeyword?.value.toLowerCase() === WHERE &&
(previousNonWhiteSpace?.isKeyword() ||
previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') ||
previousNonWhiteSpace?.is(TokenType.Operator, AND))
),
},
{
id: StatementPosition.WhereComparisonOperator,
name: StatementPosition.WhereComparisonOperator,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
previousKeyword?.value.toLowerCase() === WHERE &&
!previousNonWhiteSpace?.getPreviousNonWhiteSpaceToken()?.isOperator() &&
!currentToken?.is(TokenType.Delimiter, '.') &&
!currentToken?.isParenthesis() &&
(previousNonWhiteSpace?.isIdentifier() || previousNonWhiteSpace?.isDoubleQuotedString())
),
},
{
id: StatementPosition.WhereValue,
name: StatementPosition.WhereValue,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(previousKeyword?.value.toLowerCase() === WHERE && previousNonWhiteSpace?.isOperator()),
},
{
id: StatementPosition.AfterWhereValue,
name: StatementPosition.AfterWhereValue,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(
previousKeyword?.value.toLowerCase() === WHERE &&
(previousNonWhiteSpace?.is(TokenType.Operator, 'and') ||
previousNonWhiteSpace?.is(TokenType.Operator, 'or') ||
previousNonWhiteSpace?.isString() ||
previousNonWhiteSpace?.isNumber() ||
previousNonWhiteSpace?.is(TokenType.Parenthesis, ')') ||
previousNonWhiteSpace?.is(TokenType.Parenthesis, '()') ||
previousNonWhiteSpace?.isTemplateVariable() ||
(previousNonWhiteSpace?.is(TokenType.IdentifierQuote) &&
previousNonWhiteSpace.getPreviousNonWhiteSpaceToken()?.is(TokenType.Identifier) &&
previousNonWhiteSpace
?.getPreviousNonWhiteSpaceToken()
?.getPreviousNonWhiteSpaceToken()
?.is(TokenType.IdentifierQuote)))
);
},
},
{
id: StatementPosition.AfterGroupByKeywords,
name: StatementPosition.AfterGroupByKeywords,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
previousKeyword?.is(TokenType.Keyword, BY) &&
previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, GROUP) &&
(previousNonWhiteSpace?.is(TokenType.Keyword, BY) || previousNonWhiteSpace?.is(TokenType.Delimiter, ','))
),
},
{
id: StatementPosition.AfterGroupByFunctionArgument,
name: StatementPosition.AfterGroupByFunctionArgument,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(
previousKeyword?.is(TokenType.Keyword, BY) &&
previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, GROUP) &&
(previousNonWhiteSpace?.is(TokenType.Parenthesis, '(') || currentToken?.is(TokenType.Parenthesis, '()'))
);
},
},
{
id: StatementPosition.AfterOrderByKeywords,
name: StatementPosition.AfterOrderByKeywords,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
previousNonWhiteSpace?.is(TokenType.Keyword, BY) &&
previousNonWhiteSpace?.getPreviousKeyword()?.is(TokenType.Keyword, ORDER)
),
},
{
id: StatementPosition.AfterOrderByFunction,
name: StatementPosition.AfterOrderByFunction,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(
previousKeyword?.is(TokenType.Keyword, BY) &&
previousKeyword?.getPreviousKeyword()?.is(TokenType.Keyword, ORDER) &&
previousNonWhiteSpace?.is(TokenType.Parenthesis) &&
previousNonWhiteSpace?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Function)
),
},
{
id: StatementPosition.AfterOrderByDirection,
name: StatementPosition.AfterOrderByDirection,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) =>
Boolean(previousKeyword?.is(TokenType.Keyword, DESC) || previousKeyword?.is(TokenType.Keyword, ASC)),
},
{
id: StatementPosition.AfterIsOperator,
name: StatementPosition.AfterIsOperator,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(previousNonWhiteSpace?.is(TokenType.Operator, 'IS'));
},
},
{
id: StatementPosition.AfterIsNotOperator,
name: StatementPosition.AfterIsNotOperator,
resolve: (currentToken, previousKeyword, previousNonWhiteSpace, previousIsSlash) => {
return Boolean(
previousNonWhiteSpace?.is(TokenType.Operator, 'NOT') &&
previousNonWhiteSpace.getPreviousNonWhiteSpaceToken()?.is(TokenType.Operator, 'IS')
);
},
},
];
}
@@ -1,146 +0,0 @@
import { RegistryItem } from '@grafana/data';
import { StatementPosition, SuggestionKind } from '../types';
export interface SuggestionKindRegistryItem extends RegistryItem {
id: StatementPosition;
kind: SuggestionKind[];
}
// Registry of possible suggestions for the given statement position
export const initSuggestionsKindRegistry = (): SuggestionKindRegistryItem[] => {
return [
{
id: StatementPosition.SelectKeyword,
name: StatementPosition.SelectKeyword,
kind: [SuggestionKind.SelectKeyword],
},
{
id: StatementPosition.WithKeyword,
name: StatementPosition.WithKeyword,
kind: [SuggestionKind.WithKeyword],
},
{
id: StatementPosition.AfterSelectKeyword,
name: StatementPosition.AfterSelectKeyword,
kind: [SuggestionKind.FunctionsWithArguments, SuggestionKind.Columns, SuggestionKind.SelectMacro],
},
{
id: StatementPosition.AfterSelectFuncFirstArgument,
name: StatementPosition.AfterSelectFuncFirstArgument,
kind: [SuggestionKind.Columns],
},
{
id: StatementPosition.AfterGroupByFunctionArgument,
name: StatementPosition.AfterGroupByFunctionArgument,
kind: [SuggestionKind.Columns],
},
{
id: StatementPosition.AfterWhereFunctionArgument,
name: StatementPosition.AfterWhereFunctionArgument,
kind: [SuggestionKind.Columns],
},
{
id: StatementPosition.AfterSelectArguments,
name: StatementPosition.AfterSelectArguments,
kind: [SuggestionKind.Columns],
},
{
id: StatementPosition.AfterFromKeyword,
name: StatementPosition.AfterFromKeyword,
kind: [SuggestionKind.Tables, SuggestionKind.TableMacro],
},
{
id: StatementPosition.SelectAlias,
name: StatementPosition.SelectAlias,
kind: [SuggestionKind.Columns, SuggestionKind.FunctionsWithArguments],
},
{
id: StatementPosition.FromKeyword,
name: StatementPosition.FromKeyword,
kind: [SuggestionKind.FromKeyword],
},
{
id: StatementPosition.AfterFrom,
name: StatementPosition.AfterFrom,
kind: [
SuggestionKind.WhereKeyword,
SuggestionKind.GroupByKeywords,
SuggestionKind.OrderByKeywords,
SuggestionKind.LimitKeyword,
],
},
{
id: StatementPosition.AfterTable,
name: StatementPosition.AfterTable,
kind: [
SuggestionKind.WhereKeyword,
SuggestionKind.GroupByKeywords,
SuggestionKind.OrderByKeywords,
SuggestionKind.LimitKeyword,
],
},
{
id: StatementPosition.WhereKeyword,
name: StatementPosition.WhereKeyword,
kind: [SuggestionKind.Columns, SuggestionKind.FilterMacro, SuggestionKind.TemplateVariables],
},
{
id: StatementPosition.WhereComparisonOperator,
name: StatementPosition.WhereComparisonOperator,
kind: [SuggestionKind.ComparisonOperators],
},
{
id: StatementPosition.WhereValue,
name: StatementPosition.WhereValue,
kind: [SuggestionKind.Columns, SuggestionKind.FilterMacro, SuggestionKind.TemplateVariables],
},
{
id: StatementPosition.AfterWhereValue,
name: StatementPosition.AfterWhereValue,
kind: [
SuggestionKind.LogicalOperators,
SuggestionKind.GroupByKeywords,
SuggestionKind.OrderByKeywords,
SuggestionKind.LimitKeyword,
SuggestionKind.Columns,
SuggestionKind.TemplateVariables,
],
},
{
id: StatementPosition.AfterGroupByKeywords,
name: StatementPosition.AfterGroupByKeywords,
kind: [SuggestionKind.GroupMacro],
},
{
id: StatementPosition.AfterGroupBy,
name: StatementPosition.AfterGroupBy,
kind: [SuggestionKind.OrderByKeywords, SuggestionKind.LimitKeyword],
},
{
id: StatementPosition.AfterOrderByKeywords,
name: StatementPosition.AfterOrderByKeywords,
kind: [SuggestionKind.Columns],
},
{
id: StatementPosition.AfterOrderByFunction,
name: StatementPosition.AfterOrderByFunction,
kind: [SuggestionKind.SortOrderDirectionKeyword, SuggestionKind.LimitKeyword],
},
{
id: StatementPosition.AfterOrderByDirection,
name: StatementPosition.AfterOrderByDirection,
kind: [SuggestionKind.LimitKeyword],
},
{
id: StatementPosition.AfterIsOperator,
name: StatementPosition.AfterOrderByDirection,
kind: [SuggestionKind.NotKeyword, SuggestionKind.NullValue, SuggestionKind.BoolValues],
},
{
id: StatementPosition.AfterIsNotOperator,
name: StatementPosition.AfterOrderByDirection,
kind: [SuggestionKind.NullValue, SuggestionKind.BoolValues],
},
];
};
@@ -1,52 +0,0 @@
import { RegistryItem } from '@grafana/data';
import { monacoTypes } from '@grafana/ui';
import {
CustomSuggestion,
MacroType,
OperatorType,
PositionContext,
StatementPosition,
SuggestionKind,
} from '../types';
import { LinkedToken } from '../utils/LinkedToken';
export interface SuggestionsRegistryItem extends RegistryItem {
id: SuggestionKind;
suggestions: (position: PositionContext, m: typeof monacoTypes) => Promise<CustomSuggestion[]>;
}
export interface MacrosRegistryItem extends RegistryItem {
type: MacroType;
text: string;
args?: string[];
}
export interface FunctionsRegistryItem extends RegistryItem {}
export interface OperatorsRegistryItem extends RegistryItem {
operator: string;
type: OperatorType;
}
export type StatementPositionResolver = (
currentToken: LinkedToken | null,
previousKeyword: LinkedToken | null,
previousNonWhiteSpace: LinkedToken | null,
previousIsSlash: Boolean
) => Boolean;
export interface StatementPositionResolversRegistryItem extends RegistryItem {
id: StatementPosition;
resolve: StatementPositionResolver;
}
export type SuggestionsResolver = <T extends PositionContext = PositionContext>(
positionContext: T
) => Promise<CustomSuggestion[]>;
export interface SQLMonarchLanguage extends monacoTypes.languages.IMonarchLanguage {
keywords?: string[];
builtinFunctions?: string[];
logicalOperators?: string[];
comparisonOperators?: string[];
}
@@ -1,11 +0,0 @@
import * as testData from '../mocks/testData';
import { testStatementPosition } from './statementPosition';
import { TestQueryModel } from './types';
export const SQLEditorTestUtils = {
testData,
testStatementPosition,
};
export { TestQueryModel };
@@ -1,63 +0,0 @@
import { Registry } from '@grafana/data';
import { monacoTypes } from '@grafana/ui';
import { getMonacoMock } from '../mocks/Monaco';
import { TextModel } from '../mocks/TextModel';
import { getStatementPosition } from '../standardSql/getStatementPosition';
import { StatementPositionResolversRegistryItem } from '../standardSql/types';
import { CustomStatementPlacement, StatementPosition } from '../types';
import { linkedTokenBuilder } from '../utils/linkedTokenBuilder';
import { StatementPositionResolverTestCase } from './types';
function assertPosition(
query: string,
position: monacoTypes.IPosition,
expected: StatementPosition | string,
monacoMock: any,
resolversRegistry: Registry<StatementPositionResolversRegistryItem>
) {
const testModel = TextModel(query);
const current = linkedTokenBuilder(monacoMock, testModel as monacoTypes.editor.ITextModel, position);
const statementPosition = getStatementPosition(current, resolversRegistry);
expect(statementPosition).toContain(expected);
}
export const testStatementPosition = (
expected: StatementPosition | string,
cases: StatementPositionResolverTestCase[],
resolvers: () => CustomStatementPlacement[]
) => {
describe(`${expected}`, () => {
let MonacoMock: any;
let statementPositionResolversRegistry: Registry<StatementPositionResolversRegistryItem>;
beforeEach(() => {
const mockQueries = new Map<string, Array<Array<Pick<monacoTypes.Token, 'language' | 'offset' | 'type'>>>>();
cases.forEach((c) => mockQueries.set(c.query.query, c.query.tokens));
MonacoMock = getMonacoMock(mockQueries);
statementPositionResolversRegistry = new Registry(() => {
return resolvers().map((r) => ({
id: r.id as StatementPosition,
name: r.name || r.id,
resolve: r.resolve,
}));
});
});
// using forEach here rather than test.each as been struggling to get the arguments intepolated in test name string
cases.forEach((c) => {
test(`${c.query.query}`, () => {
assertPosition(
c.query.query,
{ lineNumber: c.position.line, column: c.position.column },
expected,
MonacoMock,
statementPositionResolversRegistry
);
});
});
});
};
@@ -1,21 +0,0 @@
SELECT column1, FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1 DESC LIMIT 10
SELECT column1, FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1 DESC LIMIT 10; SELECT column2, FROM table2 WHERE column2 = "value2" GROUP BY column1 ORDER BY column2 DESC LIMIT 10;
SELECT count(column1), FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1 DESC LIMIT 10;
SELECT count(column1), FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1 DESC LIMIT 10; SELECT count(column2), FROM table2 WHERE column2 = "value2" GROUP BY column1 ORDER BY column2 DESC LIMIT 10;
SELECT column1,
FROM table1
WHERE column1 = "value1"
GROUP BY column1 ORDER BY column1 DESC
LIMIT 10;
SELECT count(column1), column2 FROM table1 WHERE column1 = "value1" GROUP BY column1 ORDER BY column1, avg(column2) DESC LIMIT 10;
SELECT count(column1), column2
FROM table1
WHERE column1 = "value1"
GROUP BY column1 ORDER BY column1, avg(column2) DESC
LIMIT 10;
@@ -1,11 +0,0 @@
import { monacoTypes } from '@grafana/ui';
export interface TestQueryModel {
query: string;
tokens: Array<Array<Pick<monacoTypes.Token, 'language' | 'offset' | 'type'>>>;
}
export interface StatementPositionResolverTestCase {
query: TestQueryModel;
position: { line: number; column: number };
}
-430
View File
@@ -1,430 +0,0 @@
import { JsonTree } from 'react-awesome-query-builder';
import {
DataFrame,
DataQuery,
DataSourceJsonData,
MetricFindValue,
SelectableValue,
TimeRange,
toOption as toOptionFromData,
} from '@grafana/data';
import { Monaco, monacoTypes } from '@grafana/ui';
import { QueryWithDefaults } from './defaults';
import {
QueryEditorFunctionExpression,
QueryEditorGroupByExpression,
QueryEditorPropertyExpression,
} from './expressions';
import { StatementPositionResolver, SuggestionsResolver } from './standardSql/types';
import { LinkedToken } from './utils/LinkedToken';
export interface SqlQueryForInterpolation {
dataset?: string;
alias?: string;
format?: QueryFormat;
rawSql?: string;
refId: string;
hide?: boolean;
}
export interface SQLConnectionLimits {
maxOpenConns: number;
maxIdleConns: number;
connMaxLifetime: number;
}
export interface SQLOptions extends SQLConnectionLimits, DataSourceJsonData {
tlsAuth: boolean;
tlsAuthWithCACert: boolean;
timezone: string;
tlsSkipVerify: boolean;
user: string;
database: string;
url: string;
timeInterval: string;
}
export enum QueryFormat {
Timeseries = 'time_series',
Table = 'table',
}
export enum EditorMode {
Builder = 'builder',
Code = 'code',
}
export interface SQLQuery extends DataQuery {
alias?: string;
format?: QueryFormat;
rawSql?: string;
dataset?: string;
table?: string;
sql?: SQLExpression;
editorMode?: EditorMode;
rawQuery?: boolean;
}
export interface NameValue {
name: string;
value: string;
}
export type SQLFilters = NameValue[];
export interface SQLExpression {
columns?: QueryEditorFunctionExpression[];
whereJsonTree?: JsonTree;
whereString?: string;
filters?: SQLFilters;
groupBy?: QueryEditorGroupByExpression[];
orderBy?: QueryEditorPropertyExpression;
orderByDirection?: 'ASC' | 'DESC';
limit?: number;
offset?: number;
}
export interface TableSchema {
name?: string;
schema?: TableFieldSchema[];
}
export interface TableFieldSchema {
name: string;
description?: string;
type: string;
repeated: boolean;
schema: TableFieldSchema[];
}
export interface QueryRowFilter {
filter: boolean;
group: boolean;
order: boolean;
preview: boolean;
}
export const QUERY_FORMAT_OPTIONS = [
{ label: 'Time series', value: QueryFormat.Timeseries },
{ label: 'Table', value: QueryFormat.Table },
];
const backWardToOption = (value: string) => ({ label: value, value });
export const toOption = toOptionFromData ?? backWardToOption;
export interface ResourceSelectorProps {
disabled?: boolean;
className?: string;
applyDefault?: boolean;
}
// React Awesome Query builder field types.
// These are responsible for rendering the correct UI for the field.
export type RAQBFieldTypes = 'text' | 'number' | 'boolean' | 'datetime' | 'date' | 'time';
export interface SQLSelectableValue extends SelectableValue {
type?: string;
raqbFieldType?: RAQBFieldTypes;
}
export interface Aggregate {
id: string;
name: string;
description?: string;
}
export interface DB {
init?: (datasourceId?: string) => Promise<boolean>;
datasets: () => Promise<string[]>;
tables: (dataset?: string) => Promise<string[]>;
fields: (query: SQLQuery, order?: boolean) => Promise<SQLSelectableValue[]>;
validateQuery: (query: SQLQuery, range?: TimeRange) => Promise<ValidationResults>;
dsID: () => number;
dispose?: (dsID?: string) => void;
lookup: (path?: string) => Promise<Array<{ name: string; completion: string }>>;
getSqlCompletionProvider: () => LanguageCompletionProvider;
toRawSql?: (query: SQLQuery) => string;
functions: () => Promise<Aggregate[]>;
}
export interface QueryEditorProps {
db: DB;
query: QueryWithDefaults;
onChange: (query: SQLQuery) => void;
range?: TimeRange;
}
export interface ValidationResults {
query: SQLQuery;
rawSql?: string;
error: string;
isError: boolean;
isValid: boolean;
statistics?: {
TotalBytesProcessed: number;
} | null;
}
export interface SqlQueryModel {
interpolate: () => string;
quoteLiteral: (v: string) => string;
}
export interface ResponseParser {
transformMetricFindResponse: (frame: DataFrame) => MetricFindValue[];
}
export interface MetaDefinition {
name: string;
completion?: string;
kind: CompletionItemKind;
}
/**
* Provides a context for suggestions resolver
* @alpha
*/
export interface PositionContext {
position: monacoTypes.IPosition;
kind: SuggestionKind[];
statementPosition: StatementPosition[];
currentToken: LinkedToken | null;
range: monacoTypes.IRange;
}
export type CustomSuggestion = Partial<monacoTypes.languages.CompletionItem> & { label: string };
export interface CustomSuggestionKind {
id: string;
suggestionsResolver: SuggestionsResolver;
applyTo?: Array<StatementPosition | string>;
overrideDefault?: boolean;
}
export interface CustomStatementPlacement {
id: string;
name?: string;
resolve: StatementPositionResolver;
overrideDefault?: boolean;
}
export type StatementPlacementProvider = () => CustomStatementPlacement[];
export type SuggestionKindProvider = () => CustomSuggestionKind[];
export interface ColumnDefinition {
name: string;
type?: string;
description?: string;
// Text used for automplete. If not provided name is used.
completion?: string;
}
export interface TableDefinition {
name: string;
// Text used for automplete. If not provided name is used.
completion?: string;
}
export interface SQLCompletionItemProvider
extends Omit<monacoTypes.languages.CompletionItemProvider, 'provideCompletionItems'> {
/**
* Allows dialect specific functions to be added to the completion list.
* @alpha
*/
supportedFunctions?: () => Array<{
id: string;
name: string;
description?: string;
}>;
/**
* Allows dialect specific operators to be added to the completion list.
* @alpha
*/
supportedOperators?: () => Array<{
id: string;
operator: string;
type: OperatorType;
description?: string;
}>;
supportedMacros?: () => Array<{
id: string;
text: string;
type: MacroType;
args: string[];
description?: string;
}>;
/**
* Allows custom suggestion kinds to be defined and correlate them with <Custom>StatementPosition.
* @alpha
*/
customSuggestionKinds?: SuggestionKindProvider;
/**
* Allows custom statement placement definition.
* @alpha
*/
customStatementPlacement?: StatementPlacementProvider;
/**
* Allows providing a custom function for resolving db tables.
* It's up to the consumer to decide whether the columns are resolved via API calls or preloaded in the query editor(i.e. full db schema is preloades loaded).
* @alpha
*/
tables?: {
resolve: () => Promise<TableDefinition[]>;
// Allows providing a custom function for calculating the table name from the query. If not specified a default implemnentation is used.
parseName?: (t: LinkedToken) => string;
};
/**
* Allows providing a custom function for resolving table.
* It's up to the consumer to decide whether the columns are resolved via API calls or preloaded in the query editor(i.e. full db schema is preloades loaded).
* @alpha
*/
columns?: {
resolve: (table: string) => Promise<ColumnDefinition[]>;
};
/**
* TODO: Not sure whether or not we need this. Would like to avoid this kind of flexibility.
* @alpha
*/
provideCompletionItems?: (
model: monacoTypes.editor.ITextModel,
position: monacoTypes.Position,
context: monacoTypes.languages.CompletionContext,
token: monacoTypes.CancellationToken,
positionContext: PositionContext // Decorates original provideCompletionItems function with our custom statement position context
) => monacoTypes.languages.CompletionList;
}
export type LanguageCompletionProvider = (m: Monaco) => SQLCompletionItemProvider;
export enum OperatorType {
Comparison,
Logical,
}
export enum MacroType {
Value,
Filter,
Group,
Column,
Table,
}
export enum TokenType {
Parenthesis = 'delimiter.parenthesis.sql',
Whitespace = 'white.sql',
Keyword = 'keyword.sql',
Delimiter = 'delimiter.sql',
Operator = 'operator.sql',
Identifier = 'identifier.sql',
IdentifierQuote = 'identifier.quote.sql',
Type = 'type.sql',
Function = 'predefined.sql',
Number = 'number.sql',
String = 'string.sql',
Variable = 'variable.sql',
}
export enum StatementPosition {
Unknown = 'unknown',
SelectKeyword = 'selectKeyword',
WithKeyword = 'withKeyword',
AfterSelectKeyword = 'afterSelectKeyword',
AfterSelectArguments = 'afterSelectArguments',
AfterSelectFuncFirstArgument = 'afterSelectFuncFirstArgument',
SelectAlias = 'selectAlias',
AfterFromKeyword = 'afterFromKeyword',
AfterTable = 'afterTable',
SchemaFuncFirstArgument = 'schemaFuncFirstArgument',
SchemaFuncExtraArgument = 'schemaFuncExtraArgument',
FromKeyword = 'fromKeyword',
AfterFrom = 'afterFrom',
WhereKeyword = 'whereKeyword',
WhereComparisonOperator = 'whereComparisonOperator',
WhereValue = 'whereValue',
AfterWhereFunctionArgument = 'afterWhereFunctionArgument',
AfterGroupByFunctionArgument = 'afterGroupByFunctionArgument',
AfterWhereValue = 'afterWhereValue',
AfterGroupByKeywords = 'afterGroupByKeywords',
AfterGroupBy = 'afterGroupBy',
AfterOrderByKeywords = 'afterOrderByKeywords',
AfterOrderByFunction = 'afterOrderByFunction',
AfterOrderByDirection = 'afterOrderByDirection',
AfterIsOperator = 'afterIsOperator',
AfterIsNotOperator = 'afterIsNotOperator',
}
export enum SuggestionKind {
Tables = 'tables',
Columns = 'columns',
SelectKeyword = 'selectKeyword',
WithKeyword = 'withKeyword',
FunctionsWithArguments = 'functionsWithArguments',
FromKeyword = 'fromKeyword',
WhereKeyword = 'whereKeyword',
GroupByKeywords = 'groupByKeywords',
OrderByKeywords = 'orderByKeywords',
FunctionsWithoutArguments = 'functionsWithoutArguments',
LimitKeyword = 'limitKeyword',
SortOrderDirectionKeyword = 'sortOrderDirectionKeyword',
ComparisonOperators = 'comparisonOperators',
LogicalOperators = 'logicalOperators',
SelectMacro = 'selectMacro',
TableMacro = 'tableMacro',
FilterMacro = 'filterMacro',
GroupMacro = 'groupMacro',
BoolValues = 'boolValues',
NullValue = 'nullValue',
NotKeyword = 'notKeyword',
TemplateVariables = 'templateVariables',
}
// TODO: export from grafana/ui
export enum CompletionItemPriority {
High = 'a',
MediumHigh = 'd',
Medium = 'g',
MediumLow = 'k',
Low = 'q',
}
export enum CompletionItemKind {
Method = 0,
Function = 1,
Constructor = 2,
Field = 3,
Variable = 4,
Class = 5,
Struct = 6,
Interface = 7,
Module = 8,
Property = 9,
Event = 10,
Operator = 11,
Unit = 12,
Value = 13,
Constant = 14,
Enum = 15,
EnumMember = 16,
Keyword = 17,
Text = 18,
Color = 19,
File = 20,
Reference = 21,
Customcolor = 22,
Folder = 23,
TypeParameter = 24,
User = 25,
Issue = 26,
Snippet = 27,
}
export enum CompletionItemInsertTextRule {
KeepWhitespace = 1,
InsertAsSnippet = 4,
}
@@ -1,176 +0,0 @@
import { getTemplateSrv } from '@grafana/runtime';
import { monacoTypes } from '@grafana/ui';
import { TokenType } from '../types';
export class LinkedToken {
constructor(
public type: string,
public value: string,
public range: monacoTypes.IRange,
public previous: LinkedToken | null,
public next: LinkedToken | null
) {}
isKeyword(): boolean {
return this.type === TokenType.Keyword;
}
isWhiteSpace(): boolean {
return this.type === TokenType.Whitespace;
}
isParenthesis(): boolean {
return this.type === TokenType.Parenthesis;
}
isIdentifier(): boolean {
return this.type === TokenType.Identifier;
}
isString(): boolean {
return this.type === TokenType.String;
}
isNumber(): boolean {
return this.type === TokenType.Number;
}
isDoubleQuotedString(): boolean {
return this.type === TokenType.Type;
}
isVariable(): boolean {
return this.type === TokenType.Variable;
}
isFunction(): boolean {
return this.type === TokenType.Function;
}
isOperator(): boolean {
return this.type === TokenType.Operator;
}
isTemplateVariable(): boolean {
const variables = getTemplateSrv()?.getVariables();
return variables.find((v) => '$' + v.name === this.value) !== undefined;
}
is(type: TokenType, value?: string | number | boolean): boolean {
const isType = this.type === type;
return value !== undefined ? isType && compareTokenWithValue(type, this, value) : isType;
}
getPreviousNonWhiteSpaceToken(): LinkedToken | null {
let curr = this.previous;
while (curr != null) {
if (!curr.isWhiteSpace()) {
return curr;
}
curr = curr.previous;
}
return null;
}
getPreviousOfType(type: TokenType, value?: string): LinkedToken | null {
let curr = this.previous;
while (curr != null) {
const isType = curr.type === type;
if (value !== undefined ? isType && compareTokenWithValue(type, curr, value) : isType) {
return curr;
}
curr = curr.previous;
}
return null;
}
getPreviousUntil(type: TokenType, ignoreTypes: TokenType[], value?: string): LinkedToken[] | null {
let tokens: LinkedToken[] = [];
let curr = this.previous;
while (curr != null) {
if (ignoreTypes.some((t) => t === curr?.type)) {
curr = curr.previous;
continue;
}
const isType = curr.type === type;
if (value !== undefined ? isType && compareTokenWithValue(type, curr, value) : isType) {
return tokens;
}
if (!curr.isWhiteSpace()) {
tokens.push(curr);
}
curr = curr.previous;
}
return tokens;
}
getNextUntil(type: TokenType, ignoreTypes: TokenType[], value?: string): LinkedToken[] | null {
let tokens: LinkedToken[] = [];
let curr = this.next;
while (curr != null) {
if (ignoreTypes.some((t) => t === curr?.type)) {
curr = curr.next;
continue;
}
const isType = curr.type === type;
if (value !== undefined ? isType && compareTokenWithValue(type, curr, value) : isType) {
return tokens;
}
if (!curr.isWhiteSpace()) {
tokens.push(curr);
}
curr = curr.next;
}
return tokens;
}
getPreviousKeyword(): LinkedToken | null {
let curr = this.previous;
while (curr != null) {
if (curr.isKeyword()) {
return curr;
}
curr = curr.previous;
}
return null;
}
getNextNonWhiteSpaceToken(): LinkedToken | null {
let curr = this.next;
while (curr != null) {
if (!curr.isWhiteSpace()) {
return curr;
}
curr = curr.next;
}
return null;
}
getNextOfType(type: TokenType, value?: string): LinkedToken | null {
let curr = this.next;
while (curr != null) {
const isType = curr.type === type;
if (value !== undefined ? isType && compareTokenWithValue(type, curr, value) : isType) {
return curr;
}
curr = curr.next;
}
return null;
}
}
function compareTokenWithValue(type: TokenType, token: LinkedToken, value: string | number | boolean) {
return type === TokenType.Keyword || type === TokenType.Operator
? token.value.toLowerCase() === value.toString().toLowerCase()
: token.value === value;
}
@@ -1,4 +0,0 @@
export const TRIGGER_SUGGEST = {
id: 'editor.action.triggerSuggest',
title: '',
};
@@ -1,12 +0,0 @@
import { attachDebugger, createLogger } from '@grafana/ui';
let sqlEditorLogger = { logger: () => {} };
let sqlEditorLog: (...t: any[]) => void = () => {};
if (attachDebugger) {
sqlEditorLogger = createLogger('SQLEditor');
sqlEditorLog = sqlEditorLogger.logger;
attachDebugger('sqleditor', undefined, sqlEditorLogger as any);
}
export { sqlEditorLog, sqlEditorLogger };
@@ -1,8 +0,0 @@
// @ts-ignore
import sqlFormatter from 'sql-formatter-plus';
export function formatSQL(q: string) {
return sqlFormatter.format(q).replace(/(\$ \{ .* \})|(\$ __)|(\$ \w+)/g, (m: string) => {
return m.replace(/\s/g, '');
});
}
@@ -1,31 +0,0 @@
import { Registry } from '@grafana/data';
import { SuggestionKindRegistryItem } from '../standardSql/suggestionsKindRegistry';
import { StatementPosition, SuggestionKind } from '../types';
import { getSuggestionKinds } from './getSuggestionKind';
describe('getSuggestionKind', () => {
const registry = new Registry((): SuggestionKindRegistryItem[] => {
return [
{
id: StatementPosition.SelectKeyword,
name: StatementPosition.SelectKeyword,
kind: [SuggestionKind.SelectKeyword],
},
{
id: StatementPosition.AfterSelectArguments,
name: StatementPosition.AfterSelectArguments,
kind: [SuggestionKind.Columns],
},
];
});
it('should return select kind when given select keyword as position', () => {
const pos = [StatementPosition.SelectKeyword];
expect([SuggestionKind.SelectKeyword]).toEqual(getSuggestionKinds(pos, registry));
});
it('should return column kind when given AfterSelectArguments as position', () => {
const pos = [StatementPosition.AfterSelectArguments];
expect([SuggestionKind.Columns]).toEqual(getSuggestionKinds(pos, registry));
});
});
@@ -1,22 +0,0 @@
import { Registry } from '@grafana/data';
import { SuggestionKindRegistryItem } from '../standardSql/suggestionsKindRegistry';
import { StatementPosition, SuggestionKind } from '../types';
/**
* Given statement positions, returns list of suggestion kinds that apply to those positions.
*/
export function getSuggestionKinds(
statementPosition: StatementPosition[],
suggestionsKindRegistry: Registry<SuggestionKindRegistryItem>
): SuggestionKind[] {
let result: SuggestionKind[] = [];
for (let i = 0; i < statementPosition.length; i++) {
const exists = suggestionsKindRegistry.getIfExists(statementPosition[i]);
if (exists) {
result = result.concat(exists.kind);
}
}
return result;
}
@@ -1,72 +0,0 @@
import { monacoTypes } from '@grafana/ui';
import { getMonacoMock } from '../mocks/Monaco';
import { TextModel } from '../mocks/TextModel';
import { multiLineFullQuery, singleLineFullQuery } from '../mocks/testData';
import { DESC, LIMIT, SELECT } from '../standardSql/language';
import { TokenType } from '../types';
import { linkedTokenBuilder } from './linkedTokenBuilder';
describe('linkedTokenBuilder', () => {
describe('singleLineFullQuery', () => {
const testModel = TextModel(singleLineFullQuery.query);
const queriesMock = new Map();
queriesMock.set(singleLineFullQuery.query, singleLineFullQuery.tokens);
const MonacoMock = getMonacoMock(queriesMock);
it('should add correct references to next LinkedToken', () => {
const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 };
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
expect(current?.is(TokenType.Keyword, SELECT)).toBeTruthy();
expect(current?.getNextNonWhiteSpaceToken()?.is(TokenType.Identifier, 'column1')).toBeTruthy();
});
it('should add correct references to previous LinkedToken', () => {
const position: monacoTypes.IPosition = { lineNumber: 1, column: singleLineFullQuery.query.length };
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
expect(current?.is(TokenType.Number, '10')).toBeTruthy();
expect(current?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, 'LIMIT')).toBeTruthy();
expect(
current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, DESC)
).toBeTruthy();
});
});
describe('multiLineFullQuery', () => {
const testModel = TextModel(multiLineFullQuery.query);
const queriesMock = new Map();
queriesMock.set(multiLineFullQuery.query, multiLineFullQuery.tokens);
const MonacoMock = getMonacoMock(queriesMock);
it('should add LinkedToken with whitespace in case empty lines', () => {
const position: monacoTypes.IPosition = { lineNumber: 3, column: 0 };
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
expect(current).not.toBeNull();
expect(current?.isWhiteSpace()).toBeTruthy();
});
it('should add correct references to next LinkedToken', () => {
const position: monacoTypes.IPosition = { lineNumber: 1, column: 0 };
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
expect(current?.is(TokenType.Keyword, SELECT)).toBeTruthy();
expect(current?.getNextNonWhiteSpaceToken()?.is(TokenType.Identifier, 'column1')).toBeTruthy();
});
it('should add correct references to previous LinkedToken even when references spans over multiple lines', () => {
const position: monacoTypes.IPosition = { lineNumber: 6, column: 7 };
const current = linkedTokenBuilder(MonacoMock, testModel as monacoTypes.editor.ITextModel, position);
expect(current?.is(TokenType.Number, '10')).toBeTruthy();
expect(current?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, LIMIT)).toBeTruthy();
expect(
current?.getPreviousNonWhiteSpaceToken()?.getPreviousNonWhiteSpaceToken()?.is(TokenType.Keyword, DESC)
).toBeTruthy();
});
});
});
@@ -1,56 +0,0 @@
import type { monacoTypes } from '@grafana/ui';
import { TokenType } from '../types';
import { LinkedToken } from './LinkedToken';
import { Monaco } from './types';
export function linkedTokenBuilder(
monaco: Monaco,
model: monacoTypes.editor.ITextModel,
position: monacoTypes.IPosition,
languageId = 'sql'
) {
let current: LinkedToken | null = null;
let previous: LinkedToken | null = null;
const tokensPerLine = monaco.editor.tokenize(model.getValue() ?? '', languageId);
for (let lineIndex = 0; lineIndex < tokensPerLine.length; lineIndex++) {
const tokens = tokensPerLine[lineIndex];
// In case position is first column in new line, add empty whitespace token so that links are not broken
if (!tokens.length && previous) {
const token: monacoTypes.Token = {
offset: 0,
type: TokenType.Whitespace,
language: languageId,
_tokenBrand: undefined,
};
tokens.push(token);
}
for (let columnIndex = 0; columnIndex < tokens.length; columnIndex++) {
const token = tokens[columnIndex];
let endColumn =
tokens.length > columnIndex + 1 ? tokens[columnIndex + 1].offset + 1 : model.getLineLength(lineIndex + 1) + 1;
const range: monacoTypes.IRange = {
startLineNumber: lineIndex + 1,
startColumn: token.offset === 0 ? 0 : token.offset + 1,
endLineNumber: lineIndex + 1,
endColumn,
};
const value = model.getValueInRange(range);
const sqlToken: LinkedToken = new LinkedToken(token.type, value, range, previous, null);
if (monaco.Range.containsPosition(range, position)) {
current = sqlToken;
}
if (previous) {
previous.next = sqlToken;
}
previous = sqlToken;
}
}
return current;
}

Some files were not shown because too many files have changed in this diff Show More