-
Notifications
You must be signed in to change notification settings - Fork 47
Variables: support object-based values with multiple properties #1236
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
Open
grafakus
wants to merge
26
commits into
variable-value-properties
Choose a base branch
from
grafakus/var-value-props
base: variable-value-properties
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
2383d0b
feat: Add support in CustomVariable and QueryVariable
grafakus 51bfbdf
chore(QueryVariable): Remove unused class property
grafakus e49743e
fix: Add missing properties when building options
grafakus da04a3b
refactor(MultiValueVariable): Add support valueProp and textProp
grafakus c32c895
Update packages/scenes/src/variables/types.ts
grafakus 4d065f1
chore: Fix type
grafakus 7a39e48
fix(MultiValueVariable): Fix incorrect valueProperties state update
grafakus b83b465
refactor: Introduce CustomOptionsProviders
grafakus c52ad61
chore: Remove comments
grafakus 339cc7c
refactor(CustomVariable): Remove isJson config option
grafakus fcf42f3
refactor: Introduce options provider builder
grafakus 86ac6ab
feat: Enable custom options provider registration
grafakus 5a2b438
refactor: Early returns
grafakus 901a7ff
refactor
grafakus 5355a08
feat: Add demo
grafakus 29c0bdf
fix
grafakus 82a780a
feat: Better options provider + add QueryOptionsProvider
grafakus c6c76f4
refactor
grafakus 20f655b
feat(QueryVariable): Support properties received in data frames
grafakus 0fe0503
chore: Better naming
grafakus 932bd3c
fix: Fix typecheck issue
grafakus ce11620
fix: Remove unnecessary code
grafakus 0a39fea
fix: Remove unnecessary logic
grafakus f27e56a
chore: Simplify code
grafakus 34e9ca0
test(MultiVarlueVariable): Simplify unit test
grafakus 4b3b50f
chore: Remove unnecessary code
grafakus File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
packages/scenes-app/src/demos/variableWithObjectValuesDemo.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { | ||
| CustomVariable, | ||
| EmbeddedScene, | ||
| PanelBuilders, | ||
| SceneAppPage, | ||
| SceneAppPageState, | ||
| SceneFlexItem, | ||
| SceneFlexLayout, | ||
| SceneVariableSet, | ||
| VariableValueSelectors, | ||
| } from '@grafana/scenes'; | ||
| import { getEmbeddedSceneDefaults } from './utils'; | ||
|
|
||
| export function getVariableWithObjectValuesDemo(defaults: SceneAppPageState) { | ||
| return new SceneAppPage({ | ||
| ...defaults, | ||
| getScene: () => { | ||
| return new EmbeddedScene({ | ||
| ...getEmbeddedSceneDefaults(), | ||
| controls: [new VariableValueSelectors({})], | ||
| $variables: new SceneVariableSet({ | ||
| variables: [ | ||
| new CustomVariable({ | ||
| name: 'envs', | ||
| label: 'Environments', | ||
| isMulti: true, | ||
| optionsProvider: { type: 'json', valueProp: 'id', textProp: 'name' }, | ||
| query: ` | ||
| [ | ||
| { "id": 1, "name": "Development", "aws_environment": "development", "azure_environment": "dev" }, | ||
| { "id": 2, "name": "Staging", "aws_environment": "staging", "azure_environment": "stg" }, | ||
| { "id": 3, "name": "Production", "aws_environment": "prod", "azure_environment": "prd" } | ||
| ] | ||
| `, | ||
| }), | ||
| ], | ||
| }), | ||
| body: new SceneFlexLayout({ | ||
| children: [ | ||
| new SceneFlexItem({ | ||
| body: PanelBuilders.text().setTitle('AWS').setOption('content', '${envs.aws_environment}').build(), | ||
| }), | ||
| new SceneFlexItem({ | ||
| body: PanelBuilders.text().setTitle('Azure').setOption('content', '${envs.azure_environment}').build(), | ||
| }), | ||
| ], | ||
| }), | ||
| }); | ||
| }, | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
261 changes: 261 additions & 0 deletions
261
packages/scenes/src/variables/variants/CustomOptionsProviders.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| import { v4 as uuidv4 } from 'uuid'; | ||
| import { catchError, filter, from, mergeMap, Observable, of, take, throwError } from 'rxjs'; | ||
| import { sceneGraph } from '../../core/sceneGraph'; | ||
| import { VariableValueOption } from '../types'; | ||
| import { CustomVariable } from './CustomVariable'; | ||
| import { QueryVariable } from './query/QueryVariable'; | ||
| import { CoreApp, DataQueryRequest, LoadingState, PanelData, ScopedVars } from '@grafana/data'; | ||
| import { DataQuery } from '@grafana/schema'; | ||
| import { registerQueryWithController } from '../../querying/registerQueryWithController'; | ||
| import { getDataSource } from '../../utils/getDataSource'; | ||
| import { wrapInSafeSerializableSceneObject } from '../../utils/wrapInSafeSerializableSceneObject'; | ||
| import { createQueryVariableRunner } from './query/createQueryVariableRunner'; | ||
| import { toMetricFindValues } from './query/toMetricFindValues'; | ||
| import { metricNamesToVariableValues, sortVariableValues } from './query/utils'; | ||
| import { MultiValueVariable, VariableGetOptionsArgs } from './MultiValueVariable'; | ||
|
|
||
| type BuilderFunction = (variable: MultiValueVariable) => CustomOptionsProvider; | ||
|
|
||
| const OPTIONS_PROVIDERS_REGISTRY = new Map<string, BuilderFunction>([ | ||
| [ | ||
| 'csv', | ||
| (variable: MultiValueVariable) => { | ||
| if (!(variable instanceof CustomVariable)) { | ||
| throw new TypeError('Variable is not a CustomVariable'); | ||
| } | ||
| return new CsvOptionsProvider({ | ||
| csv: sceneGraph.interpolate(variable, variable.state.query), | ||
| }); | ||
| }, | ||
| ], | ||
| [ | ||
| 'json', | ||
| (variable: MultiValueVariable) => { | ||
| if (!(variable instanceof CustomVariable)) { | ||
| throw new TypeError('Variable is not a CustomVariable'); | ||
| } | ||
| return new JsonOptionsProvider({ | ||
| json: sceneGraph.interpolate(variable, variable.state.query), | ||
| valueProp: variable.state.optionsProvider.valueProp, | ||
| textProp: variable.state.optionsProvider.textProp, | ||
| }); | ||
| }, | ||
| ], | ||
| [ | ||
| 'query', | ||
| (variable: MultiValueVariable) => { | ||
| if (!(variable instanceof QueryVariable)) { | ||
| throw new TypeError('Variable is not a QueryVariable'); | ||
| } | ||
| return new QueryOptionsProvider({ | ||
| variable: variable, // TEMP, in the future, can we pass only minimal info? | ||
| }); | ||
| }, | ||
| ], | ||
| ]); | ||
|
|
||
| export function buildOptionsProvider(variable: MultiValueVariable) { | ||
| const { optionsProvider } = variable.state; | ||
| if (!optionsProvider) { | ||
| throw new Error('Variable is missing optionsProvider'); | ||
| } | ||
| if (OPTIONS_PROVIDERS_REGISTRY.has(optionsProvider.type)) { | ||
| return OPTIONS_PROVIDERS_REGISTRY.get(optionsProvider.type)!(variable); | ||
| } | ||
| throw new Error(`Unknown options provider "${optionsProvider.type}"`); | ||
| } | ||
|
|
||
| export function registerOptionsProvider(type: string, builderFn: BuilderFunction) { | ||
| if (OPTIONS_PROVIDERS_REGISTRY.has(type)) { | ||
| throw new Error(`Options provider "${type}" already registered`); | ||
| } | ||
| OPTIONS_PROVIDERS_REGISTRY.set(type, builderFn); | ||
| } | ||
|
|
||
| export type OptionsProviderSettings = { | ||
| type: string; | ||
| /** | ||
| * For object values, these settings control which properties will be used to get the text and the value of the current option | ||
| */ | ||
| valueProp?: string; | ||
| textProp?: string; | ||
| }; | ||
|
|
||
| export interface CustomOptionsProvider { | ||
| getOptions(args?: VariableGetOptionsArgs): Observable<VariableValueOption[]>; | ||
| } | ||
|
|
||
| /* CSV */ | ||
|
|
||
| interface CsvProviderParams { | ||
| csv: string; | ||
| } | ||
|
|
||
| export class CsvOptionsProvider implements CustomOptionsProvider { | ||
| public constructor(private params: CsvProviderParams) {} | ||
|
|
||
| public getOptions(): Observable<VariableValueOption[]> { | ||
| return new Observable((subscriber) => { | ||
| try { | ||
| const match = this.params.csv.match(/(?:\\,|[^,])+/g) ?? []; | ||
|
|
||
| const options = match.map((text) => { | ||
| text = text.replace(/\\,/g, ','); | ||
| const textMatch = /^\s*(.+)\s:\s(.+)$/g.exec(text) ?? []; | ||
| if (textMatch.length === 3) { | ||
| const [, key, value] = textMatch; | ||
| return { label: key.trim(), value: value.trim() }; | ||
| } else { | ||
| return { label: text.trim(), value: text.trim() }; | ||
| } | ||
| }); | ||
|
|
||
| subscriber.next(options); | ||
| subscriber.complete(); | ||
| } catch (error) { | ||
| subscriber.error(error); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /* JSON */ | ||
|
|
||
| interface JsonProviderParams { | ||
| json: string; | ||
| valueProp?: string; | ||
| textProp?: string; | ||
| } | ||
|
|
||
| export class JsonOptionsProvider implements CustomOptionsProvider { | ||
| public constructor(private params: JsonProviderParams) {} | ||
|
|
||
| private parseAndValidateJson(json: string): VariableValueOption[] { | ||
| const parsedOptions = JSON.parse(json); | ||
|
|
||
| if (!Array.isArray(parsedOptions)) { | ||
| throw new Error('Query must be a JSON array'); | ||
| } | ||
|
|
||
| if (typeof parsedOptions[0] === 'string') { | ||
| return parsedOptions.map((value) => ({ label: value.trim(), value: value.trim() })); | ||
| } | ||
|
|
||
| if (typeof parsedOptions[0] !== 'object' || parsedOptions[0] === null) { | ||
| throw new Error('Query must be a JSON array of strings or objects'); | ||
| } | ||
|
|
||
| const { valueProp, textProp } = this.params; | ||
| if (!valueProp) { | ||
| throw new Error('Variable is missing valueProp'); | ||
| } | ||
|
|
||
| return parsedOptions.map((o) => ({ | ||
| value: String(o[valueProp]).trim(), | ||
| label: String(o[textProp as any] || o[valueProp])?.trim(), | ||
| properties: o, | ||
| })); | ||
| } | ||
|
|
||
| public getOptions(): Observable<VariableValueOption[]> { | ||
| return new Observable((subscriber) => { | ||
| try { | ||
| subscriber.next(this.parseAndValidateJson(this.params.json)); | ||
| subscriber.complete(); | ||
| } catch (error) { | ||
| subscriber.error(error); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /* QUERY */ | ||
|
|
||
| interface QueryProviderParams { | ||
| variable: QueryVariable; | ||
| } | ||
|
|
||
| export class QueryOptionsProvider implements CustomOptionsProvider { | ||
| public constructor(private params: QueryProviderParams) {} | ||
|
|
||
| public getOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> { | ||
| const { variable } = this.params; | ||
| const { datasource, optionsProvider, regex, sort, staticOptions, staticOptionsOrder } = variable.state; | ||
|
|
||
| return from(getDataSource(datasource, { __sceneObject: wrapInSafeSerializableSceneObject(variable) })).pipe( | ||
| mergeMap((ds) => { | ||
| const runner = createQueryVariableRunner(ds); | ||
| const target = runner.getTarget(variable); | ||
| const request = this.getRequest(target, args.searchFilter); | ||
|
|
||
| return runner.runRequest({ variable, searchFilter: args.searchFilter }, request).pipe( | ||
| registerQueryWithController({ | ||
| type: 'QueryVariable/getValueOptions', | ||
| request: request, | ||
| origin: variable, | ||
| }), | ||
| filter((data) => data.state === LoadingState.Done || data.state === LoadingState.Error), // we only care about done or error for now | ||
| take(1), // take the first result, using first caused a bug where it in some situations throw an uncaught error because of no results had been received yet | ||
| mergeMap((data: PanelData) => { | ||
| if (data.state === LoadingState.Error) { | ||
| return throwError(() => data.error); | ||
| } | ||
| return of(data); | ||
| }), | ||
| toMetricFindValues(optionsProvider.valueProp, optionsProvider.textProp), | ||
| mergeMap((values) => { | ||
| const interpolatedRegex = regex ? sceneGraph.interpolate(variable, regex, undefined, 'regex') : ''; | ||
| let options = metricNamesToVariableValues(interpolatedRegex, sort, values); | ||
| if (staticOptions) { | ||
| const customOptions = staticOptions; | ||
| options = options.filter((option) => !customOptions.find((custom) => custom.value === option.value)); | ||
| if (staticOptionsOrder === 'after') { | ||
| options.push(...customOptions); | ||
| } else if (staticOptionsOrder === 'sorted') { | ||
| options = sortVariableValues(options.concat(customOptions), sort); | ||
| } else { | ||
| options.unshift(...customOptions); | ||
| } | ||
| } | ||
| return of(options); | ||
| }), | ||
| catchError((error) => { | ||
| if (error.cancelled) { | ||
| return of([]); | ||
| } | ||
| return throwError(() => error); | ||
| }) | ||
| ); | ||
| }) | ||
| ); | ||
| } | ||
|
|
||
| private getRequest(target: DataQuery | string, searchFilter?: string) { | ||
| const { variable } = this.params; | ||
|
|
||
| const scopedVars: ScopedVars = { | ||
| __sceneObject: wrapInSafeSerializableSceneObject(variable), | ||
| }; | ||
|
|
||
| if (searchFilter) { | ||
| scopedVars.__searchFilter = { value: searchFilter, text: searchFilter }; | ||
| } | ||
|
|
||
| const range = sceneGraph.getTimeRange(variable).state.value; | ||
|
|
||
| const request: DataQueryRequest = { | ||
| app: CoreApp.Dashboard, | ||
| requestId: uuidv4(), | ||
| timezone: '', | ||
| range, | ||
| interval: '', | ||
| intervalMs: 0, | ||
| // @ts-ignore | ||
| targets: [target], | ||
| scopedVars, | ||
| startTime: Date.now(), | ||
| }; | ||
|
|
||
| return request; | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we anticipate more formats introduction? I don't see a justification for having this complexity at this stage. Eventually when we decide to unify custom/query variable under List variable, this will become unnecessary.