Skip to content

Commit

Permalink
PromQueryBuilder: Query builder and components that can be shared wit…
Browse files Browse the repository at this point in the history
…h a loki query builder and others (#42854)
  • Loading branch information
torkelo committed Jan 31, 2022
1 parent a660ccc commit 64e1e91
Show file tree
Hide file tree
Showing 65 changed files with 3,884 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import React from 'react';
import { Icon } from '../Icon/Icon';
import { IconName } from '../../types/icon';
import { css, cx } from '@emotion/css';
import { css } from '@emotion/css';

import RCCascader from 'rc-cascader';
import { CascaderOption } from '../Cascader/Cascader';
import { onChangeCascader, onLoadDataCascader } from '../Cascader/optionMappings';
import { stylesFactory, useTheme2 } from '../../themes';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, ButtonProps } from '../Button';
import { Icon } from '../Icon/Icon';

export interface ButtonCascaderProps {
options: CascaderOption[];
children: string;
children?: string;
icon?: IconName;
disabled?: boolean;
value?: string[];
Expand All @@ -20,6 +21,9 @@ export interface ButtonCascaderProps {
onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
onPopupVisibleChange?: (visible: boolean) => void;
className?: string;
variant?: ButtonProps['variant'];
buttonProps?: ButtonProps;
hideDownIcon?: boolean;
}

const getStyles = stylesFactory((theme: GrafanaTheme2) => {
Expand All @@ -40,10 +44,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => {
});

export const ButtonCascader: React.FC<ButtonCascaderProps> = (props) => {
const { onChange, className, loadData, icon, ...rest } = props;
const { onChange, className, loadData, icon, buttonProps, hideDownIcon, variant, disabled, ...rest } = props;
const theme = useTheme2();
const styles = getStyles(theme);

// Weird way to do this bit it goes around a styling issue in Button where even null/undefined child triggers
// styling change which messes up the look if there is only single icon content.
let content: any = props.children;
if (!hideDownIcon) {
content = [props.children, <Icon key={'down-icon'} name="angle-down" className={styles.icons.right} />];
}

return (
<RCCascader
onChange={onChangeCascader(onChange)}
Expand All @@ -52,11 +63,9 @@ export const ButtonCascader: React.FC<ButtonCascaderProps> = (props) => {
{...rest}
expandIcon={null}
>
<button className={cx('gf-form-label', className)} disabled={props.disabled}>
{icon && <Icon name={icon} className={styles.icons.left} />}
{props.children}
<Icon name="angle-down" className={styles.icons.right} />
</button>
<Button icon={icon} disabled={disabled} variant={variant} {...(buttonProps ?? {})}>
{content}
</Button>
</RCCascader>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
}

&-menus {
font-size: 12px;
//font-size: 12px;
overflow: hidden;
background: $page-bg;
border: $panel-border;
Expand Down Expand Up @@ -92,7 +92,7 @@
position: relative;

&:hover {
background: $typeahead-selected-bg;
background: $colors-action-hover;
}

&-disabled {
Expand All @@ -113,12 +113,11 @@
}

&-active {
color: $typeahead-selected-color;
background: $typeahead-selected-bg;
color: $text-color-strong;
background: $colors-action-selected;

&:hover {
color: $typeahead-selected-color;
background: $typeahead-selected-bg;
background: $colors-action-hover;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { ThemeContext } from '../../../../themes';
* - noOptionsMessage & loadingMessage is of string type
* - isDisabled is renamed to disabled
*/
type LegacyCommonProps<T> = Omit<SelectCommonProps<T>, 'noOptionsMessage' | 'disabled' | 'value'>;
type LegacyCommonProps<T> = Omit<SelectCommonProps<T>, 'noOptionsMessage' | 'disabled' | 'value' | 'loadingMessage'>;

interface AsyncProps<T> extends LegacyCommonProps<T>, Omit<SelectAsyncProps<T>, 'loadingMessage'> {
loadingMessage?: () => string;
Expand Down
2 changes: 2 additions & 0 deletions packages/grafana-ui/src/components/Select/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export interface SelectCommonProps<T> {
value: SelectableValue<T> | null,
options: OptionsOrGroups<unknown, GroupBase<unknown>>
) => boolean;
/** Message to display isLoading=true*/
loadingMessage?: string;
}

export interface SelectAsyncProps<T> {
Expand Down
3 changes: 3 additions & 0 deletions packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const darkThemeVarsTemplate = (theme: GrafanaTheme2) =>
$theme-name: dark;
$colors-action-hover: ${theme.colors.action.hover};
$colors-action-selected: ${theme.colors.action.selected};
// New Colors
// -------------------------
$blue-light: ${theme.colors.primary.text};
Expand Down
3 changes: 3 additions & 0 deletions packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export const lightThemeVarsTemplate = (theme: GrafanaTheme2) =>
$theme-name: light;
$colors-action-hover: ${theme.colors.action.hover};
$colors-action-selected: ${theme.colors.action.selected};
// New Colors
// -------------------------
$blue-light: ${theme.colors.primary.text};
Expand Down
6 changes: 3 additions & 3 deletions public/app/angular/components/query_part.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,14 @@ export function functionRenderer(part: any, innerExpr: string) {
return str + parameters.join(', ') + ')';
}

export function suffixRenderer(part: QueryPartDef, innerExpr: string) {
export function suffixRenderer(part: QueryPart, innerExpr: string) {
return innerExpr + ' ' + part.params[0];
}

export function identityRenderer(part: QueryPartDef, innerExpr: string) {
export function identityRenderer(part: QueryPart, innerExpr: string) {
return part.params[0];
}

export function quotedIdentityRenderer(part: QueryPartDef, innerExpr: string) {
export function quotedIdentityRenderer(part: QueryPart, innerExpr: string) {
return '"' + part.params[0] + '"';
}
2 changes: 1 addition & 1 deletion public/app/features/query/components/QueryEditorRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ export function filterPanelDataToQuery(data: PanelData, refId: string): PanelDat
}

// Only say this is an error if the error links to the query
let state = LoadingState.Done;
let state = data.state;
const error = data.error && data.error.refId === refId ? data.error : undefined;
if (error) {
state = LoadingState.Error;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { CoreApp } from '@grafana/data';
import { LokiQueryEditorProps } from './types';
import { LokiQueryEditor } from './LokiQueryEditor';
import { LokiQueryEditorForAlerting } from './LokiQueryEditorForAlerting';
import { LokiQueryEditorSelector } from '../querybuilder/components/LokiQueryEditorSelector';
import { config } from '@grafana/runtime';

export function LokiQueryEditorByApp(props: LokiQueryEditorProps) {
const { app } = props;
Expand All @@ -11,6 +13,9 @@ export function LokiQueryEditorByApp(props: LokiQueryEditorProps) {
case CoreApp.CloudAlerting:
return <LokiQueryEditorForAlerting {...props} />;
default:
if (config.featureToggles.lokiQueryBuilder) {
return <LokiQueryEditorSelector {...props} />;
}
return <LokiQueryEditor {...props} />;
}
}
Expand Down
3 changes: 1 addition & 2 deletions public/app/plugins/datasource/loki/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { DataSourcePlugin } from '@grafana/data';
import Datasource from './datasource';

import LokiCheatSheet from './components/LokiCheatSheet';
import LokiExploreQueryEditor from './components/LokiExploreQueryEditor';
import LokiQueryEditorByApp from './components/LokiQueryEditorByApp';
import { LokiAnnotationsQueryCtrl } from './LokiAnnotationsQueryCtrl';
import { ConfigEditor } from './configuration/ConfigEditor';

export const plugin = new DataSourcePlugin(Datasource)
.setQueryEditor(LokiQueryEditorByApp)
.setConfigEditor(ConfigEditor)
.setExploreQueryField(LokiExploreQueryEditor)
.setExploreQueryField(LokiQueryEditorByApp)
.setQueryEditorHelp(LokiCheatSheet)
.setAnnotationQueryCtrl(LokiAnnotationsQueryCtrl);
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { LokiQueryModeller } from './LokiQueryModeller';
import { LokiOperationId } from './types';

describe('LokiQueryModeller', () => {
const modeller = new LokiQueryModeller();

it('Can query with labels only', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [],
})
).toBe('{app="grafana"}');
});

it('Can query with pipeline operation json', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.Json, params: [] }],
})
).toBe('{app="grafana"} | json');
});

it('Can query with pipeline operation logfmt', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.Logfmt, params: [] }],
})
).toBe('{app="grafana"} | logfmt');
});

it('Can query with line filter contains operation', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.LineContains, params: ['error'] }],
})
).toBe('{app="grafana"} |= `error`');
});

it('Can query with line filter contains operation with empty params', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.LineContains, params: [''] }],
})
).toBe('{app="grafana"}');
});

it('Can query with line filter contains not operation', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.LineContainsNot, params: ['error'] }],
})
).toBe('{app="grafana"} != `error`');
});

it('Can query with line regex filter', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.LineMatchesRegex, params: ['error'] }],
})
).toBe('{app="grafana"} |~ `error`');
});

it('Can query with line not matching regex', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.LineMatchesRegexNot, params: ['error'] }],
})
).toBe('{app="grafana"} !~ `error`');
});

it('Can query with label filter expression', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.LabelFilter, params: ['__error__', '=', 'value'] }],
})
).toBe('{app="grafana"} | __error__="value"');
});

it('Can query with label filter expression using greater than operator', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.LabelFilter, params: ['count', '>', 'value'] }],
})
).toBe('{app="grafana"} | count > value');
});

it('Can query no formatting errors operation', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.LabelFilterNoErrors, params: [] }],
})
).toBe('{app="grafana"} | __error__=""');
});

it('Can query with unwrap operation', () => {
expect(
modeller.renderQuery({
labels: [{ label: 'app', op: '=', value: 'grafana' }],
operations: [{ id: LokiOperationId.Unwrap, params: ['count'] }],
})
).toBe('{app="grafana"} | unwrap count');
});

describe('On add operation handlers', () => {
it('When adding function without range vector param should automatically add rate', () => {
const query = {
labels: [],
operations: [],
};

const def = modeller.getOperationDef('sum');
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('rate');
expect(result.operations[1].id).toBe('sum');
});

it('When adding function without range vector param should automatically add rate after existing pipe operation', () => {
const query = {
labels: [],
operations: [{ id: 'json', params: [] }],
};

const def = modeller.getOperationDef('sum');
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('json');
expect(result.operations[1].id).toBe('rate');
expect(result.operations[2].id).toBe('sum');
});

it('When adding a pipe operation after a function operation should add pipe operation first', () => {
const query = {
labels: [],
operations: [{ id: 'rate', params: [] }],
};

const def = modeller.getOperationDef('json');
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('json');
expect(result.operations[1].id).toBe('rate');
});

it('When adding a pipe operation after a line filter operation', () => {
const query = {
labels: [],
operations: [{ id: '__line_contains', params: ['error'] }],
};

const def = modeller.getOperationDef('json');
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('__line_contains');
expect(result.operations[1].id).toBe('json');
});

it('When adding a line filter operation after format operation', () => {
const query = {
labels: [],
operations: [{ id: 'json', params: [] }],
};

const def = modeller.getOperationDef('__line_contains');
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations[0].id).toBe('__line_contains');
expect(result.operations[1].id).toBe('json');
});

it('When adding a rate it should not add another rate', () => {
const query = {
labels: [],
operations: [],
};

const def = modeller.getOperationDef('rate');
const result = def.addOperationHandler(def, query, modeller);
expect(result.operations.length).toBe(1);
});
});
});

0 comments on commit 64e1e91

Please sign in to comment.