-
Notifications
You must be signed in to change notification settings - Fork 37
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 51 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
82b7219
8af2b4f
c0cba84
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,32 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { SelectableValue } from '@grafana/data'; | ||
| 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 [isLoading, setIsLoading] = useState(false); | ||
| const [options, setOptions] = useState<SelectableValue<string>[]>([]); | ||
| const handleOpenMenu = async () => { | ||
| setIsLoading(true); | ||
| setOptions(await getFieldValueOptions(props, operation.params[0] as string)); | ||
| setIsLoading(false); | ||
| }; | ||
| return ( | ||
| <InlineField> | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={isLoading} | ||
| onOpenMenu={handleOpenMenu} | ||
| options={options} | ||
| onChange={({ value = "" }) => onChange(index, value)} | ||
| value={toOption(String(value || ""))} | ||
| width="auto" | ||
| /> | ||
| </InlineField> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { SelectableValue } from '@grafana/data'; | ||
| 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(String(value || "")); | ||
| 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) => { | ||
| const parts = [fromField, toField].map(field => { | ||
| const trimmed = field.trim(); | ||
| return trimmed === "" ? "\"\"" : quoteString(trimmed); | ||
| }); | ||
| const value = parts.join(" as "); | ||
| onChange(index, value); | ||
| }; | ||
|
|
||
| const [isLoading, setIsLoading] = useState(false); | ||
| const [options, setOptions] = useState<SelectableValue<string>[]>([]); | ||
|
|
||
| const handleOpenMenu = async () => { | ||
| setIsLoading(true); | ||
| setOptions(await getFieldNameOptions(props)); | ||
| setIsLoading(false); | ||
| }; | ||
|
|
||
| return ( | ||
| <Stack> | ||
| <InlineField> | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={isLoading} | ||
| onOpenMenu={handleOpenMenu} | ||
| options={options} | ||
| onChange={({ value = "" }) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it is better to move this to separate handler
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is clearer that way |
||
| setFromField(value); | ||
| updateValue(value, toField); | ||
| }} | ||
| value={toOption(fromField)} | ||
| width="auto" | ||
| /> | ||
| </InlineField> | ||
| <div style={{ padding: '6px 0 8px 0px' }}>as</div> | ||
| <InlineField> | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={isLoading} | ||
| onOpenMenu={handleOpenMenu} | ||
| options={options} | ||
| onChange={({ value = "" }) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the same about the handler |
||
| setToField(value); | ||
| updateValue(fromField, value); | ||
| }} | ||
| value={toOption(toField)} | ||
| width="auto" | ||
| /> | ||
| </InlineField> | ||
| </Stack> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { SelectableValue } from '@grafana/data'; | ||
| 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 [isLoading, setIsLoading] = useState(false); | ||
| const [options, setOptions] = useState<SelectableValue<string>[]>([]); | ||
|
|
||
| const handleOpenMenu = async () => { | ||
| setIsLoading(true); | ||
| setOptions(await getFieldNameOptions(props)); | ||
| setIsLoading(false); | ||
| } | ||
|
|
||
| return ( | ||
| <InlineField> | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={isLoading} | ||
| onOpenMenu={handleOpenMenu} | ||
| options={options} | ||
| onChange={({ value = "" }) => onChange(index, value)} | ||
| value={toOption(String(value || ""))} | ||
| width="auto" | ||
| /> | ||
| </InlineField> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import React, { useState } from "react"; | ||
|
|
||
| import { SelectableValue } from "@grafana/data"; | ||
| 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 [isLoading, setIsLoading] = useState(false); | ||
| const [options, setOptions] = useState<SelectableValue<string>[]>([]); | ||
|
|
||
| const handleOpenMenu = async () => { | ||
| setIsLoading(true); | ||
| setOptions(await getValueTypeOptions(props)); | ||
| setIsLoading(false); | ||
| } | ||
|
|
||
| return ( | ||
| <Select<string> | ||
| allowCustomValue={true} | ||
| allowCreateWhileLoading={true} | ||
| isLoading={isLoading} | ||
| onOpenMenu={handleOpenMenu} | ||
| options={options} | ||
| onChange={({ value = "" }) => onChange(index, value)} | ||
| value={toOption(String(value || ""))} | ||
| width="auto" | ||
| /> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { SelectableValue } from '@grafana/data'; | ||
| import { QueryBuilderOperationParamEditorProps } from '@grafana/plugin-ui'; | ||
| import { ActionMeta, 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>[], action: ActionMeta) => { | ||
| let rawValues = values.map(({ value = "" }) => value.trim()).filter(Boolean); | ||
| if (action) { | ||
| if (action.action === "remove-value") { | ||
| const oldValues = getValuesFromBrackets(splitString(String(value || ""))); | ||
| rawValues = oldValues.filter((v) => v !== (action.removedValue as SelectableValue<string>).value); | ||
| } | ||
| } | ||
| const newValue = rawValues.map((v) => quoteString(v)).join(", "); | ||
| onChange(index, newValue); | ||
| } | ||
|
|
||
| const [isLoading, setIsLoading] = useState(false); | ||
| const [options, setOptions] = useState<SelectableValue<string>[]>([]); | ||
|
|
||
| const handleOpenMenu = async () => { | ||
| setIsLoading(true); | ||
| setOptions(await getFieldNameOptions(props)); | ||
| setIsLoading(false); | ||
| } | ||
|
|
||
| return ( | ||
| <MultiSelect<string> | ||
| onChange={setFields} | ||
| options={options} | ||
| value={getValuesFromBrackets(splitString(String(value || "")))} | ||
| isLoading={isLoading} | ||
| allowCustomValue | ||
| allowCreateWhileLoading | ||
| noOptionsMessage="No labels found" | ||
| loadingMessage="Loading labels" | ||
| width={20} | ||
| onOpenMenu={handleOpenMenu} | ||
| /> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| import { SelectableValue } from '@grafana/data'; | ||
| import { QueryBuilderOperationParamEditorProps } from '@grafana/plugin-ui'; | ||
| import { ActionMeta, FormatOptionLabelMeta, 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(String(value || "")); | ||
| const parsedValues = parseInputValues(str); | ||
| const [values, setValues] = useState<FieldWithPrefix[]>(parsedValues); | ||
|
|
||
| const setFields = (newValues: FieldWithPrefix[], action?: ActionMeta) => { | ||
| if (action) { | ||
| if (action.action === "remove-value") { | ||
| newValues = values.filter((v) => v.name !== (action.removedValue as any).name); | ||
| } | ||
| } | ||
| setValues(newValues); | ||
| const newValue = newValues.map((field) => { | ||
| if (field.isPrefix !== undefined) { | ||
| return field.isPrefix ? `${quoteString(field.name)}*` : `${quoteString(field.name)}`; | ||
| } | ||
| 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 [isLoading, setIsLoading] = useState(false); | ||
| const [options, setOptions] = useState<SelectableValue<FieldWithPrefix>[]>([]); | ||
|
|
||
| const formatOptionLabel = (option: SelectableValue<FieldWithPrefix>, { context }: FormatOptionLabelMeta<FieldWithPrefix>) => { | ||
| 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> | ||
| ); | ||
| } | ||
| return <>{option.label}</>; | ||
| } | ||
|
|
||
| const handleOpenMenu = async () => { | ||
| setIsLoading(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)); | ||
| setOptions(options); | ||
| setIsLoading(false); | ||
| } | ||
|
|
||
| const handleChange = (values: SelectableValue<FieldWithPrefix>[], action: ActionMeta) => { | ||
| setFields(values.map((v) => v.value || v as FieldWithPrefix), action) | ||
| } | ||
|
|
||
| return ( | ||
| <MultiSelect<FieldWithPrefix> | ||
| openMenuOnFocus | ||
| onOpenMenu={handleOpenMenu} | ||
| isLoading={isLoading} | ||
| allowCustomValue | ||
| allowCreateWhileLoading | ||
| noOptionsMessage="No labels found" | ||
| loadingMessage="Loading labels" | ||
| options={options} | ||
| value={values} | ||
| onChange={handleChange} | ||
| formatOptionLabel={formatOptionLabel} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| 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") { | ||
| const { type, value: secondValue } = value[1] || {}; | ||
| const isPrefix = type === "space" && secondValue === "*"; | ||
| return { name: unquoteString(value[0].value), isPrefix }; | ||
| } | ||
|
|
||
| const fieldValue = value[0].value; | ||
| const isPrefix = fieldValue.endsWith("*"); | ||
| return { | ||
| name: isPrefix ? fieldValue.slice(0, -1) : fieldValue, | ||
| isPrefix, | ||
| }; | ||
| } | ||
|
|
||
| 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.