Skip to content

DashboardVariables: Use Combobox behind toggle #1011

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
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

tskarhed
Copy link
Contributor

@tskarhed tskarhed commented Dec 19, 2024

  • Implements variable rendering with single Combobox
  • Adds events for reporting changed variables
  • Handles custom onSearch in Combobox rather than variable state

Closes: #96099

Related to #96091 and #98261

Followup issues:

  • Identify custom value creation in Combobox


return (
<Combobox
id={'var-' + key}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure where this var- is added for the Select

@tskarhed tskarhed marked this pull request as ready for review January 22, 2025 15:28
@tskarhed tskarhed changed the title Template Variables: Use Combobox behind toggle DashboardVariables: Use Combobox behind toggle Jan 30, 2025
Comment on lines 290 to 303
const onInputChange = useMemo(() => {
if (!model.onSearchChange) {
return;
}
return async (newInputValue: string) => {
model.onSearchChange!(newInputValue);

// Same functionality as in _updateOptionsBasedOnSearchFilter, although deboucning is managed in Combobox
return (await lastValueFrom(model.getValueOptions({ searchFilter: newInputValue }))).map((o) => ({
value: o.value.toString(),
label: o.label,
}));
};
}, [model]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is working properly, and I lack the insight into how value updates are made in Scenes to make an informed decision

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this looks different from the one on line 73?

Comment on lines 296 to 300
// If we don't use searchFilter in QueryVariable, don't bother to make a query. TODO: Proper typing
// @ts-ignore
if ('query' in model.state && !containsSearchFilter(model.state.query)) {
return;
}
Copy link
Contributor Author

@tskarhed tskarhed Feb 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some feedback to do some proper typing on this would be nice. query only exists on a subclass. I guess this breaks the class isolation somewhat.

Would setting this in the state instead make sense? Like model.state.async or something similar, and set it in the QueryVariable constructor. By default QueryVariable only runs the initial query and the rest is filtered client side.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simply call the function and use the same logic we have above (for the old select )

const res = model.onSearchChange!(newInputValue).then((result) => {
return result.map((o) => ({
value: o.value.toString(),
label: o.value.toString() === ALL_VARIABLE_VALUE ? ALL_VARIABLE_TEXT : o.label,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unclear how All should be treated in this case.

Comment on lines 290 to 303
const onInputChange = useMemo(() => {
if (!model.onSearchChange) {
return;
}
return async (newInputValue: string) => {
model.onSearchChange!(newInputValue);

// Same functionality as in _updateOptionsBasedOnSearchFilter, although deboucning is managed in Combobox
return (await lastValueFrom(model.getValueOptions({ searchFilter: newInputValue }))).map((o) => ({
value: o.value.toString(),
label: o.label,
}));
};
}, [model]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this looks different from the one on line 73?

Comment on lines 299 to 302
return (await lastValueFrom(model.getValueOptions({ searchFilter: newInputValue }))).map((o) => ({
value: o.value.toString(),
label: o.label,
}));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can remove this part

Comment on lines 296 to 300
// If we don't use searchFilter in QueryVariable, don't bother to make a query. TODO: Proper typing
// @ts-ignore
if ('query' in model.state && !containsSearchFilter(model.state.query)) {
return;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can simply call the function and use the same logic we have above (for the old select )

return;
}

return async (newInputValue: string) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think you can remove this is you simply called the function, like the current impl

return this._getNewOptions(searchFilter);
}

// Undefined for first call
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THe return value. This is because of the debounce. It can be fixed by setting { leading: true }

Copy link
Collaborator

@torkelo torkelo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems hang / crash a browser when there are many options (many values) returned by the variable query:

In this demo example:
http://localhost:3000/a/grafana-scenes-app/demos/variables/many-values?var-interval=1d&from=now-6h&to=now&timezone=browser&var-server=AA&var-pod=AAA&var-handler=pod&var-keyValue=1&var-queryVar=A&var-namespace=broken&__feature.templateVariablesUsesCombobox&var-manyOptions=CEeAKU5LETNWXReRPQY7IddhyHDK5eYQi2kdATodj6m4AYPHJ8

Tested this a bit now with a prometheus variable query that uses __searchFilter,

With this query:

  new QueryVariable({
                  name: 'namespace',
                  query: {
                    qryType: 1,
                    query: 'label_values({__name__=~"${__searchFilter}.*"},__name__)',
                    refId: 'PrometheusVariableQueryEditor-VariableQuery',
                  },
                  datasource: { uid: 'se-demo' },
                }),

The call to onSearchChange and getValueOptions causes double queries (at least on first open).

Dan we really need to move the loading state from the label to input as well causing the input to jump around (not related to this PR, just an observation)

@tskarhed
Copy link
Contributor Author

tskarhed commented Feb 4, 2025

It seems hang / crash a browser when there are many options (many values) returned by the variable query:

There is a possibility this is fixed with the latest changes to Combobox. This seems to only be an issue when first opened...
Something is creating event listeners, as they are close to 100k, looking into it.

@tskarhed
Copy link
Contributor Author

tskarhed commented Feb 4, 2025

It seems hang / crash a browser when there are many options (many values) returned by the variable query:

In this demo example: http://localhost:3000/a/grafana-scenes-app/demos/variables/many-values?var-interval=1d&from=now-6h&to=now&timezone=browser&var-server=AA&var-pod=AAA&var-handler=pod&var-keyValue=1&var-queryVar=A&var-namespace=broken&__feature.templateVariablesUsesCombobox&var-manyOptions=CEeAKU5LETNWXReRPQY7IddhyHDK5eYQi2kdATodj6m4AYPHJ8

A ton of event listeners are being created. I have no idea why. The basic use case is handled without any issues directly in Combobox.

image

@torkelo
Copy link
Collaborator

torkelo commented Feb 5, 2025

@tskarhed yea, it's strange, the combobox looks to recursive be rendering itself or something.

thought it has something to do with passing options via the onInputChange but commented and it still hangs the same way

@tskarhed
Copy link
Contributor Author

tskarhed commented Feb 5, 2025

The issue is that when the options are loaded the list virtualization isn't on somehow. No idea why. Maybe I'll test directly with 10k+ items in a dashboard. This issue doesn't appear in Prometheus or Storybook.

@tskarhed
Copy link
Contributor Author

tskarhed commented Feb 6, 2025

I still don't know exactly why this issue appears here and nowhere else. However, it seems like the initial render left the maxHeight undefined, hence the list virtualization never kicked in. I fixed this by setting arbitrary initial dimensions (which are overridden soon after measuring things).

grafana/grafana#100188

@tskarhed
Copy link
Contributor Author

tskarhed commented Feb 6, 2025

With this query:

  new QueryVariable({
                  name: 'namespace',
                  query: {
                    qryType: 1,
                    query: 'label_values({__name__=~"${__searchFilter}.*"},__name__)',
                    refId: 'PrometheusVariableQueryEditor-VariableQuery',
                  },
                  datasource: { uid: 'se-demo' },
                }),

The call to onSearchChange and getValueOptions causes double queries (at least on first open).

Posting this recording for reference.

Screen.Recording.2025-02-06.at.14.09.04.mov

It doesn't seem to happen when having an empty value. I'm looking into it.

@tskarhed
Copy link
Contributor Author

tskarhed commented Feb 6, 2025

@torkelo A call in onOpen in Combobox isn't debounced. A PR is open for it now grafana/grafana#100219

Other than that, is there anything blocking this PR, or do you feel okay approving it so we can start testing it in our instances? I assume the debounce PR will be in order soon enough.

Comment on lines 286 to 287
//@ts-ignore
model.onSearchChange && 'query' in model.state && containsSearchFilter(model.state.query),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just need to fix this now, and the empty promise return of onSearchChange.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wonder how though, need to align this impl with the other and change how options are returned a bit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding an isAsync getter on MutliVariableValue that defaults to false, but for QueryVariable has this statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it is a bit misnamed, as QueryVariable always makes at least one async request.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tskarhed yea, we need a true async mode for QueryVariable where it will always have a default value, but needs some bigger changes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Template variables: Identify "do no harm" metrics
3 participants