Skip to content
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

feat: Allow by-name component access. #148 #1870

Merged
merged 10 commits into from Mar 10, 2023
Merged

feat: Allow by-name component access. #148 #1870

merged 10 commits into from Mar 10, 2023

Conversation

mturoci
Copy link
Collaborator

@mturoci mturoci commented Mar 6, 2023

Allows for:

from h2o_wave import Q, ui, main, app


@app('/')
async def serve(q: Q):
    q.page['wizard'] = ui.form_card(box='1 1 2 4', items=[
        ui.text_xl(name='text_name', content='Wizard'),
        ui.textbox(name='textbox_name', label='Textbox'),
        ui.inline(items=[
            ui.button(name='back', label='Back'),
        ]),
    ])
    # Instead of q.page['wizard'].items[0].text_xl.content = 'foo'
    q.page['wizard'].text_name.content = 'foo'
    q.page['wizard'].textbox_name.label = 'foo'
    q.page['wizard'].back.label = 'foo'

    q.page['header'] = ui.header_card(box='4 6 4 1', title='Header', subtitle='Subtitle', secondary_items=[
        ui.button(name='button_name', label='Button'),
    ])
    q.page['header'].button_name.label = 'foo'

    q.page['example'] = ui.form_card(box='5 1 4 5', items=[
        ui.buttons([
            ui.button(name='primary_button', label='Primary', primary=True),
        ]),
    ])
    q.page['example'].primary_button.label = 'foo'

    await q.page.save()

Needs discussion

issue description: Requires modification to server-side interpreter.

@lo5 can you elaborate what you meant here? AFAIU, the whole UI state is stored client-side meaning, there is not much to be done at the server-side interpreter unless I am missing something. Apart from guarding the accessors.

page[, 'upload_progress'].caption = 'foo' # Scans all cards on page for upload_progress
page[, 'stats_table', 'search_bar'].caption = 'foo' # Scans all cards on page for search_bar inside stat_table

Wondering whether these would be useful - cc @Far0n @pascal-pfeiffer @vopani @srini-x for feedback.

Closes #148

@mturoci mturoci requested a review from lo5 as a code owner March 6, 2023 14:05
@mturoci mturoci requested a review from geomodular March 6, 2023 14:05
@Far0n
Copy link

Far0n commented Mar 6, 2023

Wondering whether these would be useful

definitely yes imho

@mturoci
Copy link
Collaborator Author

mturoci commented Mar 6, 2023

definitely yes imho

Can you expand with some examples?

@Far0n
Copy link

Far0n commented Mar 6, 2023

Can you expand with some examples?

I have no concrete example at hand, but I found these .items[x] accesses always a bit wonky, "hard to read" and error prone (due to only getting Ref type information)

@mturoci
Copy link
Collaborator Author

mturoci commented Mar 6, 2023

I found these .items[x] accesses always a bit wonky and "hard to read" and error prone (due to only getting Ref type information)

Oh, I should have made the question more clear then. Let me rephrase. The problem you just described is already implemented in this PR: q.page['card_name'].<component_name>.<prop_to_mutate>. I am more interested in the proposals:

  • page[, 'upload_progress'].caption = 'foo' # Scans all cards on page for upload_progress - When would I want to update all the components with the same name?
  • page[, 'stats_table', 'search_bar'].caption = 'foo' # Scans all cards on page for search_bar inside stat_table - do I need a hierarchy, when I can access the components directly by name?

@Far0n
Copy link

Far0n commented Mar 6, 2023

he problem you just described is already implemented in this PR

ah lol sry my bad.

    page[, 'upload_progress'].caption = 'foo' # Scans all cards on page for upload_progress - When would I want to update all the components with the same name?
    page[, 'stats_table', 'search_bar'].caption = 'foo' # Scans all cards on page for search_bar inside stat_table - do I need a hierarchy, when I can access the components directly by name?

That's nothing I find that useful as of now (no use case comes to mind).

@Far0n
Copy link

Far0n commented Mar 6, 2023

I mean there are certainly use-cases for some sort of batch processing of components like enabling/disabling or resetting, but at least personally I didn't miss that feature yet.

@lo5
Copy link
Member

lo5 commented Mar 7, 2023

can you elaborate what you meant here?

Given two scripts S1 and S2, say:

  • S1 creates the card and sets card.items[0].caption = 'foo'
  • S2 sets card.items[0].caption = 'bar'

The final caption will be bar.

Instead, say there was a way to name the item upload_progress:

  • S1 creates the card and sets card.upload_progress.caption = 'foo'
  • S2 sets card.upload_progress.caption = 'bar'

For this to work, the server needs to be aware that upload_progress is the name of a card item, can iterate through the items, and patch the correct item. Currently the patch algo can handle only slice-access by integer index: https://github.com/h2oai/wave/blob/master/card.go#L111-L116

@lo5
Copy link
Member

lo5 commented Mar 7, 2023

More thoughts: this is tricky to do primarily because card.go has no awareness of items or even what attributes a card has. One way to do this brute-force is to scan nested attributes for a name when the map case fails, but that can be expensive in the worst case, and I don't know if .name is guaranteed to be a component name universally.

The other option is to merge your PR, but with the caveat that by-name access is not supported for scripts, or for the uni-/multicast apps.

@pascal-pfeiffer
Copy link

pascal-pfeiffer commented Mar 7, 2023

Thank you for working on this @mturoci . This will be super handy when updating single items on a page.

I can't say anything about the technical implementation, but I am wondering if it works on any hierarchy level of the component?

Does any process enforce name uniqueness? If not, how would that be resolved? Would that go into the direction of your second question?

page[, 'upload_progress'].caption = 'foo' # Scans all cards on page for upload_progress - When would I want to update all the components with the same name?

@mturoci
Copy link
Collaborator Author

mturoci commented Mar 7, 2023

The other option is to merge your PR, but with the caveat that by-name access is not supported for scripts, or for the uni-/multicast apps.

I would like to avoid this if possible. Thanks for the pointers, will see what I can do.

wondering if it works on any hierarchy level of the component?

It should. If you check my example above, I target a button what is within items > inline > button.

Does any process enforce name uniqueness? If not, how would that be resolved? Would that go into the direction of your second question?

There is no enforcement from Wave side currently (#1493). The name uniqueness needs to be preserved only within the "card" scope. This means that q.page['card1'].items can have the same named items as q.page['card2']. However, if 2 or more components with the same name are present within the same card, only the first one in order gets updated currently.

Generally speaking, I think we should enforce unique names per card as it makes sense for getting the outputs via q.args and also React depends on this. Currently, it's more of an "unwritten rule".

@mturoci
Copy link
Collaborator Author

mturoci commented Mar 9, 2023

Seems like I managed to find the optimal solution to support scripts, broadcast, multicast as well without any serious perf degradation. In fact, it should run a bit faster now.

@geomodular could you please review the Go code to make sure it's idiomatic?

The parts I am not so sure about:

  • Unsafe type casts.
  • Is fillNameComponentMap function okayish since it mutates passed hashmap, thus is not a pure function.
  • I believe map[string]interface{} only stores pointers to the actual objects rather than values. Is this correct?

geomodular
geomodular previously approved these changes Mar 9, 2023
Copy link
Contributor

@geomodular geomodular left a comment

Choose a reason for hiding this comment

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

Good job, Martin. Overall I think it's fine. Take my comments as suggestions.

Unsafe type casts.

This sometimes emerges. You have interface{}; you need to cast. If by unsafe you mean without the type checking, then I'd say it's better to check. But it depends on the context.

Is fillNameComponentMap function okayish since it mutates passed hashmap, thus is not a pure function.

That's ok. We are not even trying to do functional programming in go. I'd say the function is readable, and that's the goal.

I believe map[string]interface{} only stores pointers to the actual objects rather than values. Is this correct?

Yes, that's correct. map[string]interface{} is essentially a dictionary of pointers.

card.go Outdated Show resolved Hide resolved
card.go Outdated Show resolved Hide resolved
card.go Outdated Show resolved Hide resolved
card.go Outdated Show resolved Hide resolved
card.go Outdated Show resolved Hide resolved
@mturoci
Copy link
Collaborator Author

mturoci commented Mar 10, 2023

Thanks for the review @geomodular!

If by unsafe you mean without the type checking, then I'd say it's better to check

Yes, that's exactly what I meant, especially the fillNameComponentMap part which has a lot of casts. The thing is though that all the types are expected and an err should be thrown if any of them doesn't match. Custom err is more readable than panic ofc, just not sure if worth it.

All the comments were addressed. Updated the go version via go mod edit -go 1.19 and go mod tidy.

@mturoci mturoci requested a review from geomodular March 10, 2023 09:22
geomodular
geomodular previously approved these changes Mar 10, 2023
Copy link
Contributor

@geomodular geomodular left a comment

Choose a reason for hiding this comment

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

Marvelous!

@mturoci mturoci deleted the feat/issue-148 branch March 10, 2023 10:14
@vopani
Copy link
Contributor

vopani commented Mar 15, 2023

This is a killer feature, can't wait to try it out.
Thanks for working on it! 🙏

@mturoci
Copy link
Collaborator Author

mturoci commented Mar 15, 2023

Available in nightly, feedback / bug reports encouraged since this spans all the components throughout Wave.

This is a killer feature, can't wait to try it out.

Glad to hear that!

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.

Allow by-name access to page elements
6 participants