-
Notifications
You must be signed in to change notification settings - Fork 21
Implement Grafana Query Builder #329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 8 commits
c2a6cdb
201f871
66b3d5b
ffac882
b96a69c
52f4b3e
cbc49bb
c87894a
aef6be4
111e28c
f28b814
cd5862d
50bc9e5
cd4ca7c
2aef4a9
3476058
760a477
b9bbcd6
92f195f
9fd29cf
c8220f8
a1c20cb
a291aec
b2d0fbe
838960f
8281796
238b3b2
a5527ae
12ff850
b6ae599
b45858c
8c2cd63
e265f8e
51eb66f
bd6b7aa
23af02b
db33be0
0e0e0c3
bbd152c
7d8a171
8f1c222
6efef10
ed2fee6
7842bc8
3d9de16
3270538
55d2517
7f3d219
cc26956
55f7fbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { QueryBuilderOperationParamEditorProps, toOption } from '@grafana/plugin-ui'; | ||
| import { InlineField, Select } from '@grafana/ui'; | ||
|
|
||
| import { getFieldValueOptions } from './utils/editorHelper'; | ||
|
|
||
| export default function ExactValueEditor(props: QueryBuilderOperationParamEditorProps) { | ||
| const { onChange, index, value, operation } = props; | ||
| const [state, setState] = useState({ | ||
| loading: false, | ||
| options: [] as any[], | ||
| }); | ||
|
|
||
| return ( | ||
| <InlineField> | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={state.loading} | ||
| onOpenMenu={async () => { | ||
| setState((prev) => ({ ...prev, loading: true })); | ||
| setState({ | ||
| loading: false, | ||
| options: await getFieldValueOptions(props, operation.params[0] as string), | ||
| }); | ||
| }} | ||
| options={state.options} | ||
| onChange={(value) => { | ||
| onChange(index, value.value as string); | ||
| }} | ||
| value={toOption(value as string)} | ||
| width="auto" | ||
| /> | ||
| </InlineField> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { QueryBuilderOperationParamEditorProps, toOption } from '@grafana/plugin-ui'; | ||
| import { InlineField, Stack, Select } from '@grafana/ui'; | ||
|
|
||
| import { isValue, quoteString, getValue } from '../utils/stringHandler'; | ||
| import { splitString } from '../utils/stringSplitter'; | ||
|
|
||
| import { getFieldNameOptions } from './utils/editorHelper'; | ||
|
|
||
| export default function FieldAsFieldEditor(props: QueryBuilderOperationParamEditorProps) { | ||
| const { value, onChange, index } = props; | ||
|
|
||
| const str = splitString(value as string); | ||
| let parsedFromField = ""; | ||
| let parsedToField = ""; | ||
| if (str.length === 3) { | ||
| if (isValue(str[0])) { | ||
| parsedFromField = getValue(str[0]); | ||
| } | ||
| if (str[1].type === "space" && str[1].value === "as") { | ||
| if (isValue(str[2])) { | ||
| parsedToField = getValue(str[2]); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const [fromField, setFromField] = useState<string>(parsedFromField); | ||
| const [toField, setToField] = useState<string>(parsedToField); | ||
|
||
|
|
||
| const updateValue = (fromField: string, toField: string) => { | ||
| let value = ""; | ||
| if (fromField.trim() === "") { | ||
| value += "\"\""; | ||
| } else { | ||
| value += quoteString(fromField.trim()); | ||
| } | ||
| value += " as "; | ||
| if (toField.trim() === "") { | ||
| value += "\"\""; | ||
| } else { | ||
| value += quoteString(toField.trim()); | ||
| } | ||
| onChange(index, value); | ||
| }; | ||
archef2000 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const [state, setState] = useState({ | ||
| loading: false, | ||
| options: [] as any[], | ||
| }); | ||
|
|
||
| const handleOpenMenu = async () => { | ||
| setState((prev) => ({ ...prev, loading: true })); | ||
| const options = await getFieldNameOptions(props); | ||
| setState({ | ||
| loading: false, | ||
| options, | ||
| }); | ||
| }; | ||
|
|
||
| return ( | ||
| <Stack> | ||
| <InlineField> | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={state.loading} | ||
| onOpenMenu={handleOpenMenu} | ||
| options={state.options} | ||
| onChange={(value) => { | ||
| setFromField(value.value as string); | ||
| updateValue(value.value as string, toField); | ||
| }} | ||
archef2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| value={toOption(fromField as string)} | ||
| width="auto" | ||
| /> | ||
| </InlineField> | ||
| <div style={{ padding: '6px 0 8px 0px' }}>as</div> | ||
| <InlineField> | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={state.loading} | ||
| onOpenMenu={handleOpenMenu} | ||
| options={state.options} | ||
| onChange={(value) => { | ||
| setToField(value.value as string); | ||
| updateValue(fromField, value.value as string); | ||
| }} | ||
| value={toOption(toField as string)} | ||
archef2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| width="auto" | ||
| /> | ||
| </InlineField> | ||
| </Stack> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { QueryBuilderOperationParamEditorProps, toOption } from '@grafana/plugin-ui'; | ||
| import { InlineField, Select } from '@grafana/ui'; | ||
|
|
||
| import { getFieldNameOptions } from './utils/editorHelper'; | ||
|
|
||
| export default function FieldEditor(props: QueryBuilderOperationParamEditorProps) { | ||
| const { value, onChange, index } = props; | ||
| const [state, setState] = useState({ | ||
| loading: false, | ||
| options: [] as any[], | ||
| }); | ||
|
|
||
| return ( | ||
| <InlineField> | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={state.loading} | ||
| onOpenMenu={async () => { | ||
| setState((prev) => ({ ...prev, loading: true })); | ||
| setState({ | ||
| loading: false, | ||
| options: await getFieldNameOptions(props), | ||
| }); | ||
| }} | ||
| options={state.options} | ||
| onChange={(value) => { | ||
| onChange(index, value.value as string); | ||
| }} | ||
| value={toOption(value as string)} | ||
archef2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| width="auto" | ||
| /> | ||
| </InlineField> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import React, { useState } from "react"; | ||
|
|
||
| import { QueryBuilderOperationParamEditorProps, toOption } from "@grafana/plugin-ui"; | ||
| import { Select } from "@grafana/ui"; | ||
|
|
||
| import { getValueTypeOptions } from "./utils/editorHelper"; | ||
|
|
||
| export default function FieldValueTypeEditor(props: QueryBuilderOperationParamEditorProps) { | ||
| const { value, onChange, index } = props; | ||
| const [state, setState] = useState({ | ||
| loading: false, | ||
| options: [] as any[], | ||
| }); | ||
|
|
||
| return ( | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={state.loading} | ||
| onOpenMenu={async () => { | ||
| setState((prev) => ({ ...prev, loading: true })); | ||
| setState({ | ||
| loading: false, | ||
| options: await getValueTypeOptions(props), | ||
| }); | ||
| }} | ||
| options={state.options} | ||
| onChange={(value) => { | ||
| onChange(index, value.value as string); | ||
| }} | ||
| value={toOption(value as string)} | ||
| width="auto" | ||
| /> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { SelectableValue } from '@grafana/data'; | ||
| import { QueryBuilderOperationParamEditorProps } from '@grafana/plugin-ui'; | ||
| import { MultiSelect } from '@grafana/ui'; | ||
|
|
||
| import { getValuesFromBrackets } from '../utils/operationParser'; | ||
| import { quoteString } from '../utils/stringHandler'; | ||
| import { splitString } from '../utils/stringSplitter'; | ||
|
|
||
| import { getFieldNameOptions } from './utils/editorHelper'; | ||
|
|
||
| export default function FieldsEditor(props: QueryBuilderOperationParamEditorProps) { | ||
| const { value, onChange, index } = props; | ||
|
|
||
| const setFields = (values: SelectableValue<string>[]) => { | ||
| const rawValues = values.map((v) => v.value?.trim()).filter((v) => v !== undefined && v !== null); | ||
| let value = rawValues.map((v)=> quoteString(v)).join(", "); | ||
| onChange(index, value); | ||
| } | ||
|
|
||
| const [state, setState] = useState<{ | ||
| options?: SelectableValue[]; | ||
| isLoading?: boolean; | ||
| }>({}); | ||
|
|
||
| const handleOpenMenu = async () => { | ||
| setState({ isLoading: true }); | ||
| const options = await getFieldNameOptions(props); | ||
| setState({ options, isLoading: undefined }); | ||
| } | ||
|
|
||
| return ( | ||
| <MultiSelect<string> | ||
| onChange={setFields} | ||
| options={state.options} | ||
| value={getValuesFromBrackets(splitString(value as string))} | ||
| isLoading={state.isLoading} | ||
| allowCustomValue | ||
| noOptionsMessage="No labels found" | ||
| loadingMessage="Loading labels" | ||
| width={20} | ||
| onOpenMenu={handleOpenMenu} | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { SelectableValue } from '@grafana/data'; | ||
| import { QueryBuilderOperationParamEditorProps } from '@grafana/plugin-ui'; | ||
| import { MultiSelect } from '@grafana/ui'; | ||
|
|
||
| import { quoteString, unquoteString } from '../utils/stringHandler'; | ||
| import { splitByUnescapedChar, SplitString, splitString } from '../utils/stringSplitter'; | ||
|
|
||
| import { getFieldNameOptions } from './utils/editorHelper'; | ||
|
|
||
| interface FieldWithPrefix { | ||
| name: string; | ||
| isPrefix: boolean; | ||
| } | ||
|
|
||
| export default function FieldsEditorWithPrefix(props: QueryBuilderOperationParamEditorProps) { | ||
| const { value, onChange, index } = props; | ||
|
|
||
| const str = splitString(value as string); | ||
| const parsedValues = parseInputValues(str); | ||
| const [values, setValues] = useState<FieldWithPrefix[]>(parsedValues); | ||
|
|
||
| const setFields = (values: FieldWithPrefix[]) => { | ||
| setValues(values); | ||
| const newValue = values.map((field) => { | ||
| if (field.isPrefix !== undefined) { | ||
| return field.isPrefix ? `${quoteString(field.name)}*` : `${quoteString(field.name)}`; | ||
| } else { | ||
archef2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return quoteString(field as unknown as string); | ||
| } | ||
| }).join(', '); | ||
| onChange(index, newValue); | ||
| } | ||
|
|
||
| const togglePrefix = (index: number) => { | ||
| const field = values[index]; | ||
| field.isPrefix = !field.isPrefix; | ||
| setFields(values); | ||
| }; | ||
|
|
||
| const [state, setState] = useState<{ | ||
| options?: SelectableValue<FieldWithPrefix>[]; | ||
| isLoading?: boolean; | ||
| }>({}); | ||
|
|
||
| return ( | ||
| <MultiSelect<FieldWithPrefix> | ||
| openMenuOnFocus | ||
| onOpenMenu={async () => { | ||
| setState({ isLoading: true }); | ||
| let options = await getFieldNameOptions(props); | ||
| const selectedNames = values.map(v => v.name); | ||
| options = options.filter((opt: SelectableValue<string>) => opt.value && !selectedNames.includes(opt.value)); | ||
| setState({ options, isLoading: undefined }); | ||
| }} | ||
| isLoading={state.isLoading} | ||
| allowCustomValue | ||
| noOptionsMessage="No labels found" | ||
| loadingMessage="Loading labels" | ||
| options={state.options} | ||
| value={values} | ||
| onChange={(values) => setFields(values.map((v) => v.value || v as FieldWithPrefix))} | ||
| formatOptionLabel={(option, { context }) => { | ||
| if (context === 'value') { | ||
| const field = option as FieldWithPrefix; | ||
| const handleToggle = (e: React.SyntheticEvent) => { | ||
| e.stopPropagation(); | ||
| const idx = values.findIndex((v) => (v).name === field.name); | ||
| if (idx !== -1) { | ||
| togglePrefix(idx); | ||
| } | ||
| }; | ||
| return ( | ||
| <span | ||
| tabIndex={0} | ||
| style={{ cursor: 'pointer' }} | ||
| onMouseDown={handleToggle} | ||
| > | ||
| {formatFieldLabel(field)} | ||
| </span> | ||
| ); | ||
| } | ||
archef2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return <>{option.label}</>; | ||
| }} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| const formatFieldLabel = (field: FieldWithPrefix): string => { | ||
| return field.isPrefix ? `${field.name} *` : field.name; | ||
| }; | ||
|
|
||
| const parseValue = (value: SplitString[]): FieldWithPrefix => { | ||
| if (value.length === 0 || value[0].type === "bracket") { | ||
| return { name: '', isPrefix: false }; | ||
| } | ||
| if (value[0].type === "quote") { | ||
| let isPrefix = false; | ||
| if (value.length > 1) { | ||
| isPrefix = (value[1].type === "space" && value[1].value === "*"); | ||
| } | ||
archef2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return { name: unquoteString(value[0].value), isPrefix }; | ||
| } else { | ||
archef2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let fieldValue = value[0].value; | ||
| if (fieldValue.endsWith('*')) { | ||
| return { name: fieldValue.slice(0, -1), isPrefix: true }; | ||
| } | ||
| return { name: fieldValue, isPrefix: false }; | ||
archef2000 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| const parseInputValues = (str: SplitString[]): FieldWithPrefix[] => { | ||
| let fields: FieldWithPrefix[] = []; | ||
| for (const field of splitByUnescapedChar(str, ',')) { | ||
| if (field.length > 0 && field[0].type !== "bracket") { | ||
| fields.push(parseValue(field)); | ||
| } | ||
| } | ||
| return fields; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.