Closed srini-x closed 3 years ago
The Tabs has a value option, an additional feature would be to add value to nav_group so the developer can manually choose a nav_item to highlight
It's possible I am using nav & tabs wrong, but here is some example code that would benefit from this feature. Basically whenever someone clicks a tab I would want to "unclick" or clear the menu items rather than holding the menu item state.
from typing import List
from h2o_wave import Q, expando_to_dict
from transformers import pipeline, MarianTokenizer, MarianMTModel
from .utils import *
from .navigation_elements import *
from .config import *
def create_website_content(iframe_link: str):
items = [
ui.frame(iframe_link, height='80%'),
ui.link(label='View in a Separate Tab', path=iframe_link, button=True)
]
return items
def create_image_content(image_path_wave):
items = [
ui.markup(f'<img style="max-width: 100%; max-height: 100%;" src="{image_path_wave}" />')
]
return items
def create_dai_content(dai_url):
items = []
if dai_url is not None:
items = items + create_website_content(dai_url)
else:
items.append(ui.text('Please configure an instance of Driverless AI!'))
items.append(ui.button(name='go_to_configure', caption='Go to App Configuration', primary=True))
return items
def create_wavecloud_content(app_name: str):
# TODO: Handle id not existing because application was upgraded
app_id = wave_app_homepage[app_name]
return create_website_content('https://wave.h2o.ai/apps/' + app_id)
def create_markdown_content(file_path: str):
with open(file_path) as f:
contents = f.read()
return [ui.text(contents)]
def get_dataprep_content():
items = [
ui.text_m('## Driverless AI Managed Data Processing Steps\n\n'
'* Checks for and handles missing values in your dataset\n\n'
'* Tests low cardinality features as numeric and categorical to find the best representation for '
'your dataset\n\n'
'* Runs special algorithms for imbalanced classification or skewed regression problems\n\n'
'* Identifies any columns that may cause your model to overfit and removes them or warns the user '
'based on severity\n\n'
'* Identifies any columns which have shifted between the train and test dataset, warning the user '
'that there may be time dependency in their data\n\n'
)
]
return items
def get_snowflakequery_content(mojo_name, realtime_query, sf_username, sf_password, sf_account, sf_warehouse):
if sf_username is None:
return [
ui.text('Please configure an instance of Snowflake!'),
ui.button(name='go_to_configure', caption='Go to App Configuration', primary=True)
]
if mojo_name is None:
model = "riskmodel"
else:
model = mojo_name
items = [
ui.textbox(name='sql', label='Model Name', multiline=False, value=model),
ui.button(name='snowflake_realtime_button', label='Real-Time Predict', primary=True),
ui.link(label='Go To Snowflake',
path='https://h20_ai_partner.snowflakecomputing.com/console#/internal/worksheet', button=False)
]
if realtime_query:
query = sf_query_part1 + model + sf_query_part2
result, err = query_snowflake(query, sf_username, sf_password, sf_account, sf_warehouse)
if err:
items.append(ui.message_bar(type='error', text=err))
else:
fields, rows = result
items.append(ui.text(make_markdown_table(fields, rows)))
else:
items.append(ui.text('Execute a query to view results here.'))
return items
def get_textgeneration_content(starter_text, num_words_to_generate):
if num_words_to_generate is None:
text = "H2O is one of the best tools for any machine learning team to have when dealing with large volumes " \
"of data. H2O helps improve"
else:
text = starter_text
items = [
ui.text_xl('Enter text to begin generation: '),
ui.textbox(name='input_text', label='', value=text, multiline=True),
ui.separator(),
ui.slider(name='num_words_to_generate', label="Number of Words to Generation", min=5, max=50, step=1,
value=num_words_to_generate if num_words_to_generate else 20),
ui.button(name='online_generate_button', label='Generate', primary=True)
]
if num_words_to_generate is not None:
model = pipeline("text-generation")
result = model(text, max_length=num_words_to_generate + len(text.split()), do_sample=False)[0]
items.append(ui.text(result["generated_text"]))
else:
items.append(ui.text('Enter text to see results.'))
return items
def get_texttranslation_content(online_translate_button, source_language, input_text, target_language):
if online_translate_button is None:
input_text = 'This is some sample text, hello!'
if source_language is None:
source_language = 'en'
target_language = 'fr'
language_name_map_dict = {'ar': 'Arabic', 'fr': 'French', 'de': 'German', 'en': 'English', 'hi': 'Hindi',
'ja': 'Japanese', 'ru': 'Russian', 'es': 'Spanish', 'zh': 'Chinese'}
language_translation_dict = {
'ar': ['de', 'en', 'es', 'fr', 'ru'],
'fr': ['ar', 'de', 'en', 'es', 'ru'],
'de': ['ar', 'de', 'en', 'es', 'fr'],
'en': ['ar', 'de', 'es', 'fr', 'hi', 'ru', 'zh'],
'hi': ['en'],
'ja': ['ar', 'de', 'en', 'es', 'fr', 'ru'],
'ru': ['ar', 'en', 'es', 'fr'],
'es': ['ar', 'de', 'en', 'es', 'fr', 'ru'],
'zh': ['de', 'en']
}
source_language_choices = []
for l_abrv, l_name in language_name_map_dict.items():
source_language_choices.append(ui.choice(l_abrv, l_name))
target_language_choices = []
for l_abrv in language_translation_dict[source_language]:
target_language_choices.append(ui.choice(l_abrv, language_name_map_dict[l_abrv]))
items = [
ui.text_xl('Enter text to translate: '),
ui.textbox(name='input_text', label='', value=input_text, multiline=True),
ui.separator(),
ui.dropdown(name='source_language_trigger', label='Select the language of input text:', value=source_language,
required=True, trigger=True, choices=source_language_choices),
ui.dropdown(name='target_language', label='Select the target language for translation:', value=target_language,
required=True, trigger=False, choices=target_language_choices),
ui.button(name='online_translate_button', label='Generate', primary=True)
]
if online_translate_button is not None:
model_name = f'Helsinki-NLP/opus-mt-{source_language}-{target_language}'
model = MarianMTModel.from_pretrained(model_name)
tokenizer = MarianTokenizer.from_pretrained(model_name)
batch = tokenizer.prepare_seq2seq_batch(src_texts=[input_text])
gen = model.generate(**batch)
words: List[str] = tokenizer.batch_decode(gen, skip_special_tokens=True)
items.append(ui.text(words[0]))
else:
items.append(ui.text('Enter text to see translation here.'))
return items
def get_configure_content(dai_configuration_updated, dai_address, dai_username, dai_password,
sf_configuration_updated, sf_username, sf_password, sf_account, sf_warehouse):
# TODO: check if credentials are valid
items = []
if dai_configuration_updated:
items.append(ui.message_bar('success', 'Driverless AI credentials have been saved!'))
if sf_configuration_updated:
items.append(ui.message_bar('success', 'Snowflake credentials have been saved!'))
# TODO: can currently only save one at a time
items = items + configure_dai(dai_address, dai_username, dai_password)
items = items + configure_snowflake(sf_username, sf_password, sf_account, sf_warehouse)
return items
def get_contributors_content():
return []
def get_demooverview_content():
items = [
ui.text_m('## Things you should know\n\n'
'* This application was build for the Gartner Cloud AI Developer Services 2021 Magic Quad\n\n'
'* To show DAI in browser, see this thread: '
'https://h2oai.slack.com/archives/CFF49UG65/p1602377325257700\n\n'
'* For the best demo experience, log into the following ahead of time in a seperate browser tab: '
'puddle.h2o.ai, your DAI instance, https://ui.mm-demo.h2o.ai/, wave.h2o.ai\n\n'
'* Currently, you cannot run jupyter notebooks in the H2O AI Cloud, so the H20-3 sections have '
'been replaced with static scripts\n\n'
'* You must provide your own Driverless AI instance, you can configure this in the Home / '
'Configure this Application section\n\n'
'* You must provide your own Snowflake instance, you can configure this in the Home / '
'Configure this Application section\n\n'
'* Credentials are saved at the user level, so if you close your tab and come back later you will '
'still be able to show DAI & Snowflake\n\n'
'* The Snowflake demo is currently hardcoded to Lending Club and will only work with mojos on that '
'data\n\n'
'* See the following tabs in this section for the script that was used for the Garnter '
'presentation\n\n'
'* Run the data pumper script for best MLOps demo experience\n\n'
'* Currently, UCF101 Video Analysis, NLP Explainer, and Real time drift detection are private apps '
'owned by Michelle so you may or may not get an error page on these. I will update this application '
'when those apps have been updated by the main develope\n\nr'
'* I had issues installing pytorch in wave.h2o.ai so that section is currently removed\n\n'
"* Data prep & feature engineering don't render because they are not https"
)
]
return items
async def initialize_app(q: Q):
# log at the client level
q.client.logger = logger_setup('gartner_caids')
q.client.logger.info('Application has been initialized.')
# q.user.driverlessai_configured = False
# Upload images and gifs used in this application
# these are at that app level as they are the same for all users and will never change
q.app.image_resources = {}
keys_png = ['home', 'h2o_ai_cloud', 'h2o_products', 'h2o_wave', 'dataprep_pipeline', 'image_workflow',
'ocr_workflow', 'github_logo']
for i in keys_png:
image_path_local = './static/' + i + '.png'
image_path_wave, = await q.site.upload([image_path_local])
q.app.image_resources[i] = image_path_wave
keys_gif = ['automl', 'language', 'vision']
for i in keys_gif:
image_path_local = './static/' + i + '.gif'
image_path_wave, = await q.site.upload([image_path_local])
q.app.image_resources[i] = image_path_wave
# Setup UI elements on the page
q.page['meta'] = ui.meta_card(box='', title='H2O AIaaS')
q.page['title'] = ui.header_card(
box='1 1 2 1',
title='AI as a Service',
subtitle='H2O AI Cloud Demonstration',
icon='AppIconDefault',
icon_color=application_color,
)
q.page['topbar'] = ui.form_card(
box='3 1 -1 1',
items=[ui.tabs(name='service_tab', value='home', items=get_services_tabs())]
)
q.page['sidebar'] = ui.nav_card(box='1 2 2 -1', items=[])
q.page['content'] = ui.form_card(box='3 2 -1 -1', items=[])
async def main(q: Q):
topbar = q.page['topbar']
sidebar = q.page['sidebar']
content = q.page['content']
topbar_selection = q.args.service_tab
sidebar_selection = q.args['#']
content_selection = q.args.center_content_tabs
content_tabs = q.client.content_tabs # retrieve center tab ui element from previous screen
content_items = [ui.text('404')] # TODO: create an oopsies page
if not q.client.app_initialized:
await initialize_app(q)
topbar_selection = 'home'
q.client.app_initialized = True
q.client.logger.info('Request handled.')
if len(expando_to_dict(q.args)):
q.client.logger.info('Main Function Arguments:\n%s\n', q.args)
if len(expando_to_dict(q.client)):
q.client.logger.info('Client Variables:\n%s\n', q.client)
if len(expando_to_dict(q.user)):
q.client.logger.info('User Variables:\n%s\n', q.user)
if len(expando_to_dict(q.app)):
q.client.logger.info('Application Variables:\n%s\n', q.app)
if q.args.snowflake_realtime_button:
content_selection = 'automl/modelops/snowflake'
content.items = [ui.progress('Scoring data in Snowflake')]
await q.page.save()
if q.args.online_generate_button:
content_selection = 'language/generation/generation'
content.items = [ui.progress('Generating data using GPT-2')]
await q.page.save()
if q.args.online_translate_button:
content_selection = 'language/translation/translation'
content.items = [ui.progress('Translating data using MarianMTModel')]
await q.page.save()
if q.args.dai_configure_button:
q.user.driverlessai_configured = True
q.user.dai_address = q.args.dai_address
q.user.dai_username = q.args.dai_username
q.user.dai_password = q.args.dai_password
sidebar_selection = 'about/configure'
if q.args.sf_configure_button:
q.user.snowflake_configured = True
q.user.sf_username = q.args.sf_username
q.user.sf_password = q.args.sf_password
q.user.sf_account = q.args.sf_account
q.user.sf_warehouse = q.args.sf_warehouse
sidebar_selection = 'about/configure'
if q.args.go_to_configure:
topbar_selection = 'home'
sidebar_selection = 'about/configure'
if topbar_selection is not None:
topbar.items[0].tabs.value = topbar_selection # Make appropriate top tab selected
content_tabs = []
del q.page['about_bar'] # This is only needed for the home page, not services pages
if topbar_selection == 'home':
q.page['about_bar'] = ui.nav_card(box='1 7 2 -1', items=get_about_navigation())
sidebar.items = get_home_navigation()
content_items = create_image_content(q.app.image_resources['home'])
elif topbar_selection == 'automl':
sidebar.items = get_automl_navigation()
content_items = create_image_content(q.app.image_resources['automl'])
elif topbar_selection == 'language':
sidebar.items = get_language_navigation()
content_items = create_image_content(q.app.image_resources['language'])
elif topbar_selection == 'vision':
sidebar.items = get_vision_navigation()
content_items = create_image_content(q.app.image_resources['vision'])
if sidebar_selection is not None:
content_tabs = []
# Home sidebar options
if sidebar_selection == 'home/platform':
content_items = create_image_content(q.app.image_resources['h2o_ai_cloud'])
q.page['about_bar'].items[0].nav_group.value = None
elif sidebar_selection == 'home/products':
content_items = create_image_content(q.app.image_resources['h2o_products'])
elif sidebar_selection == 'home/wave':
content_items = create_image_content(q.app.image_resources['h2o_wave'])
# About sidebar options
elif sidebar_selection == 'about/configure':
content_items = get_configure_content(q.args.dai_configure_button, q.user.dai_address,
q.user.dai_username, q.user.dai_password,
q.args.sf_configure_button, q.user.sf_username,
q.user.sf_password, q.user.sf_account, q.user.sf_warehouse)
elif sidebar_selection == 'about/script':
content_tabs = get_demo_tabs()
elif sidebar_selection == 'about/sourcecode':
content_items = [ui.link(path='https://github.com/h2oai/q-product-demo/tree/caids',
label='View me on GitHub!')] + python_code_content('run.py')
elif sidebar_selection == 'about/contributors':
content_items = get_contributors_content()
# AutoML sidebar options
elif sidebar_selection == 'automl/provision':
content_tabs = get_automl_provision_tabs()
elif sidebar_selection == 'automl/automl':
content_tabs = get_automl_automl_tabs()
elif sidebar_selection == 'automl/mli':
content_tabs = get_automl_mli_tabs()
elif sidebar_selection == 'automl/dataprep':
content_tabs = get_automl_dataprep_tabs()
elif sidebar_selection == 'automl/featureengineering':
content_tabs = get_automl_featureengineering_tabs()
elif sidebar_selection == 'automl/documentation':
content_tabs = get_automl_documentation_tabs()
elif sidebar_selection == 'automl/modelops':
content_tabs = get_automl_modelops_tabs()
elif sidebar_selection == 'automl/modelmanagement':
content_tabs = get_automl_modelmanagement_tabs()
# Language sidebar options
elif sidebar_selection == 'language/analytics':
content_tabs = get_language_analytics_tabs()
elif sidebar_selection == 'language/generation':
content_tabs = get_language_generation_tabs()
elif sidebar_selection == 'language/translation':
content_tabs = get_language_translation_tabs()
# Vision sidebar options
elif sidebar_selection == 'vision/image':
content_tabs = get_vision_image_tabs()
elif sidebar_selection == 'vision/video':
content_tabs = get_vision_video_tabs()
elif sidebar_selection == 'vision/ocr':
content_tabs = get_vision_ocr_tabs()
# Landing page for each section is the first tab
if len(content_tabs) > 0:
content_selection = content_tabs[0].name
if content_selection is not None:
if content_selection == 'about/script/home':
content_items = get_demooverview_content()
elif content_selection == 'about/script/intro':
content_items = create_markdown_content('./scripts/caids_2021_intro_script.md')
elif content_selection == 'about/script/automl':
content_items = create_markdown_content('./scripts/caids_2021_automl_script.md')
elif content_selection == 'about/script/language':
content_items = create_markdown_content('./scripts/caids_2021_language_script.md')
elif content_selection == 'about/script/vision':
content_items = create_markdown_content('./scripts/caids_2021_vision_script.md')
elif content_selection == 'automl/provision/puddle':
content_items = create_website_content('https://puddle.h2o.ai')
elif content_selection == 'automl/automl/dai':
content_items = create_dai_content(q.user.dai_address)
elif content_selection == 'automl/automl/oss':
content_items = python_code_content('automl.py')
elif content_selection == 'automl/automl/citizends':
content_items = create_wavecloud_content('citizends')
elif content_selection == 'automl/automl/umap':
content_items = create_wavecloud_content('umap')
elif content_selection == 'automl/mli/dai':
content_items = create_dai_content(q.user.dai_address)
elif content_selection == 'automl/mli/oss':
content_items = python_code_content('autoexplain.py')
elif content_selection == 'automl/mli/rml':
content_items = create_wavecloud_content('rml')
elif content_selection == 'automl/dataprep/dai':
content_items = get_dataprep_content()
elif content_selection == 'automl/dataprep/pipeline':
content_items = create_image_content(q.app.image_resources['dataprep_pipeline'])
elif content_selection == 'automl/dataprep/recipes':
content_items = create_website_content('http://catalog.h2o.ai/')
elif content_selection == 'automl/featureengineering/recipes':
content_items = create_website_content('http://catalog.h2o.ai/')
elif content_selection == 'automl/documentation/dai':
content_items = create_dai_content(q.user.dai_address)
elif content_selection == 'automl/documentation/oss':
content_items = python_code_content('autodoc.py')
elif content_selection == 'automl/modelops/dai':
content_items = create_dai_content(q.user.dai_address)
elif content_selection == 'automl/modelops/snowflake':
content_items = get_snowflakequery_content(q.args.sql, q.args.snowflake_realtime_button,
q.user.sf_username, q.user.sf_password, q.user.sf_account,
q.user.sf_warehouse)
elif content_selection == 'automl/modelops/modelops':
content_items = create_website_content('https://ui.mm-demo.h2o.ai')
elif content_selection == 'automl/modelmanagement/drift':
content_items = create_wavecloud_content('drift')
elif content_selection == 'automl/modelmanagement/backtesting':
content_items = create_wavecloud_content('backtesting')
elif content_selection == 'automl/modelmanagement/robustness':
content_items = create_wavecloud_content('adversarial')
elif content_selection == 'language/analytics/textpreprocessing':
content_items = create_wavecloud_content('textpreprocessing')
elif content_selection == 'language/analytics/datalabeling':
content_items = create_wavecloud_content('datalabeling')
elif content_selection == 'language/analytics/dai':
content_items = create_dai_content(q.user.dai_address)
elif content_selection == 'language/analytics/languageexplainer':
content_items = create_wavecloud_content('languageexplainer')
elif content_selection == 'language/generation/generation':
content_items = get_textgeneration_content(q.args.input_text, q.args.num_words_to_generate)
elif content_selection == 'language/translation/translation':
content_items = get_texttranslation_content(q.args.online_translate_button, q.args.source_language,
q.args.input_text, q.args.target_language)
elif content_selection == 'vision/image/howto':
content_items = create_image_content(q.app.image_resources['image_workflow'])
elif content_selection == 'vision/image/dai':
content_items = create_dai_content(q.user.dai_address)
elif content_selection == 'vision/video/video':
content_items = create_wavecloud_content('deepfake')
elif content_selection == 'vision/ocr/howto':
content_items = create_image_content(q.app.image_resources['ocr_workflow'])
elif content_selection == 'vision/ocr/ocr':
content_items = create_wavecloud_content('ocr')
q.client.content_tabs = content_tabs # Save section that we are in for next function call
if len(content_tabs) > 0:
content.items = [ui.tabs(name='center_content_tabs', value=content_selection, items=content_tabs),
ui.frame(content=' ', height="20px")] + content_items
else:
content.items = content_items
await q.page.save()
I don't think this is a good idea. From UX and code point of view, you should have only a single navigation / nav card in your app (the code above has 2). Having multiple causes syncing problems with active / highlighted state as mentioned in the issue. The usecase for navigation in general is fairly simple: Allow users to explore different parts of your app and show them where they are currently. In my opinion adding an option to manually remove the state is not a good idea because:
My suggestion is that each app should have a single navigation that would take you to different pages. This also means that the tab navigation in Tour may not be a good example and should be probably rethought. I think of tabs as a "Javascript only" or "memory only" navigation that shows and hides groups of content in a certain context (part of the detail page, various aspects of some product etc.), but not for using it as a replacement for a proper navigation. @lo5 your thoughts on this?
@mturoci I think there's a big picture problem being touch on here which is: the data science app developers keep, as a group, trying to do stuff that is bad
Specifically on this point though. Let's say an app has one navigation bar, since the developer has no way to "click" and highlight a tab manually when someone starts the app the "home" nav item isn't selected (and can't be) which feels weird to me as a user (but definitely isn't a big deal)
@mtanco agree on initial selection. My previous comment refers to "unclicking". My proposal is: Let app developer set initial selection and don't care about the following state (it's going to be handled client side only when clicking). Encourage only single nav_card
per app.
I am fine with this proposal 👍
As for the encouragement, app templates will definitely help so hopefully there will be less of this type of issue in the future
Adding here as it's related to this whole navigation conversation - Tour.py looks a little weird to me when you 1. click a menu item and then 2. use the next button to go forward. Then the old, "wrong" menu item is selected.
I get why, and I understand why we don't want to let people change it, but it also seems like if you click next it tour it should automatically "select" the appropriate menu item, purely from a UX perspective. Should a different widget than menu be used? @mturoci
Good question @mtanco. The whole UX is not the best. I tried to search for some examples, but could not find any that would have both Previous / Next buttons together with nav. Tour of Go has no sidebar and let's you go one step at time only (which I believe is a true tour-like experience). Our tour is more like docs site / playground I would say. I would personally remove the Previous / Next buttons. However, if we still want to keep the original solution, we have no other option than just allow programatic control over nav selection.
Right now, if there are multiple
nav_groups
in thetab_card
, the selectednav_item
in everynav_group
is highlighted. When looking at the picture below, it is unclear which tab is the active tab.The solution is to highlight only the last clicked tab/
nav_item
.Alternatives considered used nested
ui.command
but that doesn't highlight anything. and also works different than anav_item
Additional context
Add any other context or screenshots about the feature request here.![image](https://user-images.githubusercontent.com/16831326/98420220-8dda8600-203b-11eb-9ff7-cc7da0bb51c6.png)