Core code editor/builder components (#52421)

* migrate experimental to core grafana - update refs

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Scott Lepper 2022-07-20 12:50:08 -04:00 committed by GitHub
parent feb9960f96
commit de956fc3d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
157 changed files with 6644 additions and 210 deletions

View File

@ -5362,15 +5362,7 @@ exports[`better eslint`] = {
[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"], [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.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "3"]
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/features/playlist/PlaylistNewPage.test.tsx: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"]
], ],
"public/app/features/playlist/PlaylistSrv.test.ts:5381": [ "public/app/features/playlist/PlaylistSrv.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -5530,11 +5522,39 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "7"], [0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Do not use any type assertions.", "8"] [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": [ "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.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"] [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": [ "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.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@ -254,7 +254,6 @@
"@grafana/aws-sdk": "0.0.37", "@grafana/aws-sdk": "0.0.37",
"@grafana/data": "workspace:*", "@grafana/data": "workspace:*",
"@grafana/e2e-selectors": "workspace:*", "@grafana/e2e-selectors": "workspace:*",
"@grafana/experimental": "^0.0.2-canary.32",
"@grafana/google-sdk": "0.0.3", "@grafana/google-sdk": "0.0.3",
"@grafana/lezer-logql": "^0.0.14", "@grafana/lezer-logql": "^0.0.14",
"@grafana/runtime": "workspace:*", "@grafana/runtime": "workspace:*",

View File

@ -0,0 +1,23 @@
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),
}),
}));

View File

@ -0,0 +1,79 @@
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,
},
}),
};
});

View File

@ -0,0 +1,9 @@
import React from 'react';
import { Stack } from './Stack';
interface EditorFieldGroupProps {}
export const EditorFieldGroup: React.FC<EditorFieldGroupProps> = ({ children }) => {
return <Stack gap={1}>{children}</Stack>;
};

View File

@ -0,0 +1,25 @@
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),
}),
}));

View File

@ -0,0 +1,49 @@
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>
);
}

View File

@ -0,0 +1,30 @@
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),
}),
};
};

View File

@ -0,0 +1,13 @@
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>
);
};

View File

@ -0,0 +1,25 @@
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,
}),
};
};

View File

@ -0,0 +1,10 @@
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 }} />;
};

View File

@ -0,0 +1,89 @@
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,
}),
}));

View File

@ -0,0 +1,54 @@
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,
},
},
}),
}));

View File

@ -0,0 +1,40 @@
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',
},
]),
}));

View File

@ -0,0 +1,30 @@
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),
}),
}));

View File

@ -0,0 +1,13 @@
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';

View File

@ -265,3 +265,4 @@ export * from './PanelChrome/types';
export { EmotionPerfTest } from './ThemeDemos/EmotionPerfTest'; export { EmotionPerfTest } from './ThemeDemos/EmotionPerfTest';
export { Label as BrowserLabel } from './BrowserLabel/Label'; export { Label as BrowserLabel } from './BrowserLabel/Label';
export { PanelContainer } from './PanelContainer/PanelContainer'; export { PanelContainer } from './PanelContainer/PanelContainer';
export * from './QueryEditor';

View File

@ -5,8 +5,7 @@ import { useDispatch } from 'react-redux';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Card, FilterInput, Icon, Pagination, Select, Stack, TagList, useStyles2 } from '@grafana/ui';
import { Card, FilterInput, Icon, Pagination, Select, TagList, useStyles2 } from '@grafana/ui';
import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants'; import { DEFAULT_PER_PAGE_PAGINATION } from 'app/core/constants';
import { getQueryParamValue } from 'app/core/utils/query'; import { getQueryParamValue } from 'app/core/utils/query';
import { FolderState } from 'app/types'; import { FolderState } from 'app/types';

View File

@ -2,8 +2,7 @@ import { css } from '@emotion/css';
import React, { FormEvent } from 'react'; import React, { FormEvent } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Label, Tooltip, Input, Icon, useStyles2, Stack } from '@grafana/ui';
import { Label, Tooltip, Input, Icon, useStyles2 } from '@grafana/ui';
interface Props { interface Props {
className?: string; className?: string;

View File

@ -4,8 +4,7 @@ import React, { useCallback } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { useStyles2, Field, Input, InputControl, Label, Tooltip, Icon, Stack } from '@grafana/ui';
import { useStyles2, Field, Input, InputControl, Label, Tooltip, Icon } from '@grafana/ui';
import { FolderPickerFilter } from 'app/core/components/Select/FolderPicker'; import { FolderPickerFilter } from 'app/core/components/Select/FolderPicker';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { DashboardSearchHit } from 'app/features/search/types'; import { DashboardSearchHit } from 'app/features/search/types';

View File

@ -3,8 +3,7 @@ import { isEmpty } from 'lodash';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { GrafanaTheme2 } from '@grafana/data/src'; import { GrafanaTheme2 } from '@grafana/data/src';
import { Stack } from '@grafana/experimental'; import { Stack, useStyles2 } from '@grafana/ui';
import { useStyles2 } from '@grafana/ui';
import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler'; import { useRulesSourcesWithRuler } from '../../../hooks/useRuleSourcesWithRuler';
import { RuleFormType } from '../../../types/rule-form'; import { RuleFormType } from '../../../types/rule-form';

View File

@ -3,9 +3,8 @@ import { debounce } from 'lodash';
import React, { FormEvent, useState } from 'react'; import React, { FormEvent, useState } from 'react';
import { DataSourceInstanceSettings, GrafanaTheme, SelectableValue } from '@grafana/data'; import { DataSourceInstanceSettings, GrafanaTheme, SelectableValue } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { DataSourcePicker } from '@grafana/runtime'; import { DataSourcePicker } from '@grafana/runtime';
import { Button, Field, Icon, Input, Label, RadioButtonGroup, Tooltip, useStyles } from '@grafana/ui'; import { Button, Field, Icon, Input, Label, RadioButtonGroup, Stack, Tooltip, useStyles } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto'; import { PromAlertingRuleState, PromRuleType } from 'app/types/unified-alerting-dto';

View File

@ -3,8 +3,7 @@ import { debounce, uniqueId } from 'lodash';
import React, { FormEvent, useState } from 'react'; import React, { FormEvent, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Label, Icon, Input, Tooltip, RadioButtonGroup, useStyles2, Button, Field, Stack } from '@grafana/ui';
import { Label, Icon, Input, Tooltip, RadioButtonGroup, useStyles2, Button, Field } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { SilenceState } from 'app/plugins/datasource/alertmanager/types'; import { SilenceState } from 'app/plugins/datasource/alertmanager/types';

View File

@ -3,8 +3,7 @@ import React, { FC, useMemo } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { GrafanaTheme2, dateMath } from '@grafana/data'; import { GrafanaTheme2, dateMath } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Icon, useStyles2, Link, Button, Stack } from '@grafana/ui';
import { Icon, useStyles2, Link, Button } from '@grafana/ui';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types';

View File

@ -1,8 +1,7 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental'; import { Button, Checkbox, Form, Stack, TextArea } from '@grafana/ui';
import { Button, Checkbox, Form, TextArea } from '@grafana/ui';
import { DashboardModel } from 'app/features/dashboard/state'; import { DashboardModel } from 'app/features/dashboard/state';
import { SaveDashboardData, SaveDashboardOptions } from '../types'; import { SaveDashboardData, SaveDashboardOptions } from '../types';

View File

@ -3,8 +3,7 @@ import { saveAs } from 'file-saver';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Button, ClipboardButton, HorizontalGroup, Stack, stylesFactory, TextArea, useTheme } from '@grafana/ui';
import { Button, ClipboardButton, HorizontalGroup, stylesFactory, TextArea, useTheme } from '@grafana/ui';
import { SaveDashboardFormProps } from '../types'; import { SaveDashboardFormProps } from '../types';

View File

@ -3,8 +3,7 @@ import React, { ChangeEvent, FC } from 'react';
import { useToggle } from 'react-use'; import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Button, Icon, InlineField, Stack, TextArea, useStyles2 } from '@grafana/ui';
import { Button, Icon, InlineField, TextArea, useStyles2 } from '@grafana/ui';
import { ExpressionQuery } from '../types'; import { ExpressionQuery } from '../types';

View File

@ -4,9 +4,8 @@ import { Subscription } from 'rxjs';
import { DataFrame } from '@grafana/data'; import { DataFrame } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Stack } from '@grafana/experimental';
import { config, RefreshEvent } from '@grafana/runtime'; import { config, RefreshEvent } from '@grafana/runtime';
import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder } from '@grafana/ui'; import { Button, ClipboardButton, JSONFormatter, LoadingPlaceholder, Stack } from '@grafana/ui';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils'; import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
import { PanelModel } from 'app/features/dashboard/state'; import { PanelModel } from 'app/features/dashboard/state';

View File

@ -1,9 +1,19 @@
import React from 'react'; import React from 'react';
import { locationUtil } from '@grafana/data'; import { locationUtil } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { Button, LinkButton, Input, Switch, RadioButtonGroup, Form, Field, InputControl, FieldSet } from '@grafana/ui'; import {
Button,
LinkButton,
Input,
Switch,
RadioButtonGroup,
Form,
Field,
InputControl,
FieldSet,
Stack,
} from '@grafana/ui';
import { getConfig } from 'app/core/config'; import { getConfig } from 'app/core/config';
import { OrgRole, useDispatch } from 'app/types'; import { OrgRole, useDispatch } from 'app/types';

View File

@ -2,11 +2,11 @@ import React, { useCallback, useEffect, useState } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { QueryEditorProps } from '@grafana/data'; import { QueryEditorProps } from '@grafana/data';
import { EditorMode, Space } from '@grafana/experimental'; import { Space } from '@grafana/ui';
import { SqlDatasource } from '../datasource/SqlDatasource'; import { SqlDatasource } from '../datasource/SqlDatasource';
import { applyQueryDefaults } from '../defaults'; import { applyQueryDefaults } from '../defaults';
import { SQLQuery, QueryRowFilter, SQLOptions } from '../types'; import { SQLQuery, QueryRowFilter, SQLOptions, EditorMode } from '../types';
import { haveColumns } from '../utils/sql.utils'; import { haveColumns } from '../utils/sql.utils';
import { QueryHeader } from './QueryHeader'; import { QueryHeader } from './QueryHeader';

View File

@ -2,11 +2,23 @@ import React, { useCallback, useState } from 'react';
import { useCopyToClipboard } from 'react-use'; import { useCopyToClipboard } from 'react-use';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorHeader, EditorMode, EditorRow, FlexItem, InlineSelect, Space } from '@grafana/experimental'; import {
import { Button, InlineField, InlineSwitch, RadioButtonGroup, Select, Tooltip } from '@grafana/ui'; Button,
EditorField,
EditorHeader,
EditorRow,
FlexItem,
InlineField,
InlineSelect,
InlineSwitch,
RadioButtonGroup,
Select,
Space,
Tooltip,
} from '@grafana/ui';
import { QueryWithDefaults } from '../defaults'; import { QueryWithDefaults } from '../defaults';
import { SQLQuery, QueryFormat, QueryRowFilter, QUERY_FORMAT_OPTIONS, DB } from '../types'; import { SQLQuery, QueryFormat, QueryRowFilter, QUERY_FORMAT_OPTIONS, DB, EditorMode } from '../types';
import { defaultToRawSql } from '../utils/sql.utils'; import { defaultToRawSql } from '../utils/sql.utils';
import { ConfirmModal } from './ConfirmModal'; import { ConfirmModal } from './ConfirmModal';

View File

@ -0,0 +1,2 @@
export * from './query-editor-raw';
export * from './visual-query-builder';

View File

@ -1,10 +1,10 @@
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { LanguageCompletionProvider, SQLEditor } from '@grafana/experimental'; import { LanguageCompletionProvider, SQLQuery } from '../../types';
import { SQLQuery } from '../../types';
import { formatSQL } from '../../utils/formatSQL'; import { formatSQL } from '../../utils/formatSQL';
import { SQLEditor } from './SQLEditor';
type Props = { type Props = {
query: SQLQuery; query: SQLQuery;
onChange: (value: SQLQuery, processQuery: boolean) => void; onChange: (value: SQLQuery, processQuery: boolean) => void;

View File

@ -0,0 +1,135 @@
## 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[]>;
};
}
```

View File

@ -0,0 +1,405 @@
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)!;
}

View File

@ -0,0 +1 @@
export { SQLEditor, LanguageDefinition } from './SQLEditor';

View File

@ -1,8 +1,7 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data'; import { SelectableValue, toOption } from '@grafana/data';
import { AccessoryButton, EditorList, InputGroup } from '@grafana/experimental'; import { AccessoryButton, EditorList, InputGroup, Select } from '@grafana/ui';
import { Select } from '@grafana/ui';
import { QueryEditorGroupByExpression } from '../../expressions'; import { QueryEditorGroupByExpression } from '../../expressions';
import { SQLExpression } from '../../types'; import { SQLExpression } from '../../types';

View File

@ -2,8 +2,7 @@ import { uniqueId } from 'lodash';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data'; import { SelectableValue, toOption } from '@grafana/data';
import { EditorField, InputGroup, Space } from '@grafana/experimental'; import { EditorField, Input, InputGroup, RadioButtonGroup, Select, Space } from '@grafana/ui';
import { Input, RadioButtonGroup, Select } from '@grafana/ui';
import { SQLExpression } from '../../types'; import { SQLExpression } from '../../types';
import { setPropertyField } from '../../utils/sql.utils'; import { setPropertyField } from '../../utils/sql.utils';

View File

@ -3,8 +3,7 @@ import { uniqueId } from 'lodash';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { SelectableValue, toOption } from '@grafana/data'; import { SelectableValue, toOption } from '@grafana/data';
import { EditorField, Stack } from '@grafana/experimental'; import { Button, EditorField, Select, Stack, useStyles2 } from '@grafana/ui';
import { Button, Select, useStyles2 } from '@grafana/ui';
import { QueryEditorExpressionType, QueryEditorFunctionExpression } from '../../expressions'; import { QueryEditorExpressionType, QueryEditorFunctionExpression } from '../../expressions';
import { SQLExpression } from '../../types'; import { SQLExpression } from '../../types';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { EditorField, EditorRow, EditorRows } from '@grafana/experimental'; import { EditorRows, EditorRow, EditorField } from '@grafana/ui';
import { DB, QueryEditorProps, QueryRowFilter } from '../../types'; import { DB, QueryEditorProps, QueryRowFilter } from '../../types';
import { QueryToolbox } from '../query-editor-raw/QueryToolbox'; import { QueryToolbox } from '../query-editor-raw/QueryToolbox';

View File

@ -0,0 +1 @@
export { GroupByRow } from './GroupByRow';

View File

@ -1,4 +1,4 @@
import { OperatorType } from '@grafana/experimental'; import { OperatorType } from './types';
export const AGGREGATE_FNS = [ export const AGGREGATE_FNS = [
{ {

View File

@ -1,6 +1,4 @@
import { EditorMode } from '@grafana/experimental'; import { EditorMode, QueryFormat, SQLQuery } from './types';
import { QueryFormat, SQLQuery } from './types';
import { createFunctionField, setGroupByField } from './utils/sql.utils'; import { createFunctionField, setGroupByField } from './utils/sql.utils';
export function applyQueryDefaults(q?: SQLQuery): SQLQuery { export function applyQueryDefaults(q?: SQLQuery): SQLQuery {

View File

@ -0,0 +1,22 @@
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';

View File

@ -0,0 +1,26 @@
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 };

View File

@ -0,0 +1,21 @@
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 };

View File

@ -0,0 +1,214 @@
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',
},
],
],
};

View File

@ -0,0 +1,229 @@
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',
},
],
],
};

View File

@ -0,0 +1,269 @@
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',
},
],
],
};

View File

@ -0,0 +1,6 @@
import { TestQueryModel } from '../../test-utils/types';
export const singleLineEmptyQuery: TestQueryModel = {
query: '',
tokens: [],
};

View File

@ -0,0 +1,196 @@
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[][],
};

View File

@ -0,0 +1,209 @@
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',
},
],
],
};

View File

@ -0,0 +1,250 @@
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',
},
],
],
};

View File

@ -0,0 +1,385 @@
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',
},
],
],
};

View File

@ -0,0 +1,415 @@
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',
},
],
],
};

View File

@ -0,0 +1,9 @@
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';

View File

@ -0,0 +1,24 @@
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;

View File

@ -0,0 +1,273 @@
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",
},
]
`);
});
});

View File

@ -0,0 +1,34 @@
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);
};

View File

@ -0,0 +1,184 @@
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
);
});

View File

@ -0,0 +1,31 @@
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;
}

View File

@ -0,0 +1,880 @@
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' }],
],
},
};

View File

@ -0,0 +1,67 @@
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.',
},
];

View File

@ -0,0 +1,425 @@
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(')');
}

View File

@ -0,0 +1,244 @@
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')
);
},
},
];
}

View File

@ -0,0 +1,146 @@
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],
},
];
};

View File

@ -0,0 +1,52 @@
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[];
}

View File

@ -0,0 +1,11 @@
import * as testData from '../mocks/testData';
import { testStatementPosition } from './statementPosition';
import { TestQueryModel } from './types';
export const SQLEditorTestUtils = {
testData,
testStatementPosition,
};
export { TestQueryModel };

View File

@ -0,0 +1,63 @@
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
);
});
});
});
};

View File

@ -0,0 +1,21 @@
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;

View File

@ -0,0 +1,11 @@
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 };
}

View File

@ -9,7 +9,7 @@ import {
TimeRange, TimeRange,
toOption as toOptionFromData, toOption as toOptionFromData,
} from '@grafana/data'; } from '@grafana/data';
import { CompletionItemKind, EditorMode, LanguageCompletionProvider } from '@grafana/experimental'; import { Monaco, monacoTypes } from '@grafana/ui';
import { QueryWithDefaults } from './defaults'; import { QueryWithDefaults } from './defaults';
import { import {
@ -17,6 +17,8 @@ import {
QueryEditorGroupByExpression, QueryEditorGroupByExpression,
QueryEditorPropertyExpression, QueryEditorPropertyExpression,
} from './expressions'; } from './expressions';
import { StatementPositionResolver, SuggestionsResolver } from './standardSql/types';
import { LinkedToken } from './utils/LinkedToken';
export interface SqlQueryForInterpolation { export interface SqlQueryForInterpolation {
dataset?: string; dataset?: string;
@ -49,6 +51,11 @@ export enum QueryFormat {
Table = 'table', Table = 'table',
} }
export enum EditorMode {
Builder = 'builder',
Code = 'code',
}
export interface SQLQuery extends DataQuery { export interface SQLQuery extends DataQuery {
alias?: string; alias?: string;
format?: QueryFormat; format?: QueryFormat;
@ -174,3 +181,250 @@ export interface MetaDefinition {
completion?: string; completion?: string;
kind: CompletionItemKind; 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,
}

View File

@ -0,0 +1,176 @@
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;
}

View File

@ -0,0 +1,4 @@
export const TRIGGER_SUGGEST = {
id: 'editor.action.triggerSuggest',
title: '',
};

View File

@ -0,0 +1,12 @@
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 };

View File

@ -0,0 +1,31 @@
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));
});
});

View File

@ -0,0 +1,22 @@
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;
}

View File

@ -0,0 +1,72 @@
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();
});
});
});

View File

@ -0,0 +1,56 @@
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;
}

View File

@ -0,0 +1,19 @@
import { monacoTypes } from '@grafana/ui';
import { CompletionItemKind, CompletionItemPriority } from '../types';
export const toCompletionItem = (
value: string,
range: monacoTypes.IRange,
rest: Partial<monacoTypes.languages.CompletionItem> = {}
) => {
const item: monacoTypes.languages.CompletionItem = {
label: value,
insertText: value,
kind: CompletionItemKind.Field,
sortText: CompletionItemPriority.Medium,
range,
...rest,
};
return item;
};

View File

@ -0,0 +1,57 @@
import { FROM, SCHEMA, SELECT } from '../standardSql/language';
import { TokenType } from '../types';
import { LinkedToken } from './LinkedToken';
export const getSelectToken = (currentToken: LinkedToken | null) =>
currentToken?.getPreviousOfType(TokenType.Keyword, SELECT) ?? null;
export const getSelectStatisticToken = (currentToken: LinkedToken | null) => {
const assumedStatisticToken = getSelectToken(currentToken)?.getNextNonWhiteSpaceToken();
return assumedStatisticToken?.isVariable() || assumedStatisticToken?.isFunction() ? assumedStatisticToken : null;
};
export const getMetricNameToken = (currentToken: LinkedToken | null) => {
// statistic function is followed by `(` and then an argument
const assumedMetricNameToken = getSelectStatisticToken(currentToken)?.next?.next;
return assumedMetricNameToken?.isVariable() || assumedMetricNameToken?.isIdentifier() ? assumedMetricNameToken : null;
};
export const getFromKeywordToken = (currentToken: LinkedToken | null) => {
const selectToken = getSelectToken(currentToken);
return selectToken?.getNextOfType(TokenType.Keyword, FROM);
};
export const getNamespaceToken = (currentToken: LinkedToken | null) => {
const fromToken = getFromKeywordToken(currentToken);
const nextNonWhiteSpace = fromToken?.getNextNonWhiteSpaceToken();
if (
nextNonWhiteSpace?.isDoubleQuotedString() ||
(nextNonWhiteSpace?.isVariable() && nextNonWhiteSpace?.value.toUpperCase() !== SCHEMA)
) {
// schema is not used
return nextNonWhiteSpace;
} else if (nextNonWhiteSpace?.isKeyword() && nextNonWhiteSpace.next?.is(TokenType.Parenthesis, '(')) {
// schema is specified
const assumedNamespaceToken = nextNonWhiteSpace.next?.next;
if (assumedNamespaceToken?.isDoubleQuotedString() || assumedNamespaceToken?.isVariable()) {
return assumedNamespaceToken;
}
}
return null;
};
export const getTableToken = (currentToken: LinkedToken | null) => {
const fromToken = getFromKeywordToken(currentToken);
const nextNonWhiteSpace = fromToken?.getNextNonWhiteSpaceToken();
if (nextNonWhiteSpace?.isVariable()) {
// TODO: resolve column from variable?
return null;
} else if (nextNonWhiteSpace?.isKeyword() && nextNonWhiteSpace.next?.is(TokenType.Parenthesis, '(')) {
return null;
} else {
return nextNonWhiteSpace;
}
return null;
};

View File

@ -0,0 +1,14 @@
import { monacoTypes } from '@grafana/ui';
export interface Editor {
tokenize: (value: string, languageId: string) => monacoTypes.Token[][];
}
export interface Range {
containsPosition: (range: monacoTypes.IRange, position: monacoTypes.IPosition) => boolean;
}
export interface Monaco {
editor: Editor;
Range: Range;
}

View File

@ -1,8 +1,7 @@
// Libraries // Libraries
import React, { FC } from 'react'; import React, { FC } from 'react';
import { Stack } from '@grafana/experimental'; import { Card, Stack } from '@grafana/ui';
import { Card } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
// Types // Types

View File

@ -2,8 +2,7 @@ import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental'; import { Button, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
import { Button, ToolbarButton, useStyles2 } from '@grafana/ui';
import { SceneObjectBase } from '../core/SceneObjectBase'; import { SceneObjectBase } from '../core/SceneObjectBase';
import { SceneObject, SceneLayoutChildState, SceneComponentProps, SceneLayout } from '../core/types'; import { SceneObject, SceneLayoutChildState, SceneComponentProps, SceneLayout } from '../core/types';

View File

@ -11,8 +11,7 @@ import {
LabelsToFieldsMode, LabelsToFieldsMode,
LabelsToFieldsOptions, LabelsToFieldsOptions,
} from '@grafana/data/src/transformations/transformers/labelsToFields'; } from '@grafana/data/src/transformations/transformers/labelsToFields';
import { Stack } from '@grafana/experimental'; import { InlineField, InlineFieldRow, RadioButtonGroup, Select, FilterPill, Stack } from '@grafana/ui';
import { InlineField, InlineFieldRow, RadioButtonGroup, Select, FilterPill } from '@grafana/ui';
const modes: Array<SelectableValue<LabelsToFieldsMode>> = [ const modes: Array<SelectableValue<LabelsToFieldsMode>> = [
{ value: LabelsToFieldsMode.Columns, label: 'Columns' }, { value: LabelsToFieldsMode.Columns, label: 'Columns' },

View File

@ -2,9 +2,8 @@ import React, { useState } from 'react';
import { useDebounce } from 'react-use'; import { useDebounce } from 'react-use';
import { QueryEditorProps, toOption } from '@grafana/data'; import { QueryEditorProps, toOption } from '@grafana/data';
import { EditorField, EditorRows } from '@grafana/experimental';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Input } from '@grafana/ui'; import { EditorField, EditorRows, Input } from '@grafana/ui';
import { INPUT_WIDTH } from '../constants'; import { INPUT_WIDTH } from '../constants';
import CloudMonitoringDatasource from '../datasource'; import CloudMonitoringDatasource from '../datasource';

View File

@ -1,8 +1,7 @@
import React, { FC, useMemo } from 'react'; import React, { FC, useMemo } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField } from '@grafana/experimental'; import { EditorField, Select } from '@grafana/ui';
import { Select } from '@grafana/ui';
import { getAggregationOptionsByMetric } from '../../functions'; import { getAggregationOptionsByMetric } from '../../functions';
import { MetricDescriptor, MetricKind, ValueTypes } from '../../types'; import { MetricDescriptor, MetricKind, ValueTypes } from '../../types';

View File

@ -1,8 +1,7 @@
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import React, { FunctionComponent, useState } from 'react'; import React, { FunctionComponent, useState } from 'react';
import { EditorRow, EditorField } from '@grafana/experimental'; import { EditorField, EditorRow, Input } from '@grafana/ui';
import { Input } from '@grafana/ui';
import { SELECT_WIDTH } from '../../constants'; import { SELECT_WIDTH } from '../../constants';

View File

@ -1,7 +1,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorRow, EditorField, EditorFieldGroup, Stack } from '@grafana/experimental'; import { EditorRow, EditorFieldGroup, EditorField, Stack } from '@grafana/ui';
import { ALIGNMENT_PERIODS, SELECT_WIDTH } from '../../constants'; import { ALIGNMENT_PERIODS, SELECT_WIDTH } from '../../constants';
import CloudMonitoringDatasource from '../../datasource'; import CloudMonitoringDatasource from '../../datasource';

View File

@ -1,8 +1,7 @@
import React, { FunctionComponent } from 'react'; import React, { FunctionComponent } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/experimental'; import { EditorField, EditorRow, HorizontalGroup, Switch } from '@grafana/ui';
import { HorizontalGroup, Switch } from '@grafana/ui';
import { GRAPH_PERIODS, SELECT_WIDTH } from '../../constants'; import { GRAPH_PERIODS, SELECT_WIDTH } from '../../constants';
import { PeriodSelect } from '../index'; import { PeriodSelect } from '../index';

View File

@ -1,8 +1,7 @@
import React, { FunctionComponent, useMemo } from 'react'; import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup, EditorRow } from '@grafana/experimental'; import { EditorField, EditorFieldGroup, EditorRow, MultiSelect } from '@grafana/ui';
import { MultiSelect } from '@grafana/ui';
import { SYSTEM_LABELS } from '../../constants'; import { SYSTEM_LABELS } from '../../constants';
import { labelsToGroupedOptions } from '../../functions'; import { labelsToGroupedOptions } from '../../functions';

View File

@ -1,8 +1,7 @@
import React, { FunctionComponent, useMemo } from 'react'; import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue, toOption } from '@grafana/data'; import { SelectableValue, toOption } from '@grafana/data';
import { AccessoryButton, EditorRow, EditorField, EditorList } from '@grafana/experimental'; import { AccessoryButton, EditorField, EditorList, EditorRow, HorizontalGroup, Select } from '@grafana/ui';
import { HorizontalGroup, Select } from '@grafana/ui';
import { labelsToGroupedOptions, stringArrayToFilters } from '../../functions'; import { labelsToGroupedOptions, stringArrayToFilters } from '../../functions';

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorRows } from '@grafana/experimental'; import { EditorRows } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource'; import CloudMonitoringDatasource from '../../datasource';
import { getAlignmentPickerData } from '../../functions'; import { getAlignmentPickerData } from '../../functions';

View File

@ -3,8 +3,7 @@ import { startCase, uniqBy } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { EditorRow, EditorField, EditorFieldGroup } from '@grafana/experimental'; import { EditorField, EditorFieldGroup, EditorRow, getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
import { getSelectStyles, Select, useStyles2, useTheme2 } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource'; import CloudMonitoringDatasource from '../../datasource';
import { MetricDescriptor } from '../../types'; import { MetricDescriptor } from '../../types';

View File

@ -1,8 +1,7 @@
import React, { FunctionComponent, useMemo } from 'react'; import React, { FunctionComponent, useMemo } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/experimental'; import { EditorField, EditorRow, RadioButtonGroup } from '@grafana/ui';
import { RadioButtonGroup } from '@grafana/ui';
import { getAlignmentPickerData } from '../../functions'; import { getAlignmentPickerData } from '../../functions';
import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../../types'; import { MetricDescriptor, MetricKind, MetricQuery, PreprocessorType, ValueTypes } from '../../types';

View File

@ -1,8 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/experimental'; import { EditorField, EditorRow, Select } from '@grafana/ui';
import { Select } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource'; import CloudMonitoringDatasource from '../../datasource';

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { EditorHeader, FlexItem, InlineSelect } from '@grafana/experimental'; import { EditorHeader, FlexItem, InlineSelect, RadioButtonGroup } from '@grafana/ui';
import { RadioButtonGroup } from '@grafana/ui';
import { QUERY_TYPES } from '../../constants'; import { QUERY_TYPES } from '../../constants';
import { EditorMode, CloudMonitoringQuery, QueryType, SLOQuery, MetricQuery } from '../../types'; import { EditorMode, CloudMonitoringQuery, QueryType, SLOQuery, MetricQuery } from '../../types';

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/experimental'; import { EditorField, EditorRow, Select } from '@grafana/ui';
import { Select } from '@grafana/ui';
import CloudMonitoringDatasource from '../../datasource'; import CloudMonitoringDatasource from '../../datasource';
import { SLOQuery } from '../../types'; import { SLOQuery } from '../../types';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup, EditorRow, Stack } from '@grafana/experimental'; import { EditorRow, EditorFieldGroup, EditorField, Stack } from '@grafana/ui';
import { ALIGNMENT_PERIODS } from '../../constants'; import { ALIGNMENT_PERIODS } from '../../constants';
import CloudMonitoringDatasource from '../../datasource'; import CloudMonitoringDatasource from '../../datasource';

View File

@ -1,8 +1,7 @@
import React from 'react'; import React from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/experimental'; import { EditorField, EditorRow, Select } from '@grafana/ui';
import { Select } from '@grafana/ui';
import { SELECTORS } from '../../constants'; import { SELECTORS } from '../../constants';
import CloudMonitoringDatasource from '../../datasource'; import CloudMonitoringDatasource from '../../datasource';

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