diff --git a/.changeset/happy-ways-rescue.md b/.changeset/happy-ways-rescue.md new file mode 100644 index 0000000..cf8f48e --- /dev/null +++ b/.changeset/happy-ways-rescue.md @@ -0,0 +1,5 @@ +--- +"trackio": minor +--- + +feat:bump to gradio 6.0, make `trackio` compatible, and fix related issues diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11c98aa..2c46d9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,8 +48,8 @@ jobs: - name: Install Python dependencies run: | - uv pip install --system -e .[dev,tensorboard] - uv pip install --system pytest + uv pip install --system -e .[dev,tensorboard] --prerelease=allow + uv pip install --system pytest --prerelease=allow - name: Install Playwright run: | diff --git a/README.md b/README.md index 5fe6685..455b605 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ Supported query parameters: - `project`: (string) Filter the dashboard to show only a specific project - `metrics`: (comma-separated list) Filter the dashboard to show only specific metrics, e.g. `train_loss,train_accuracy` - `sidebar`: (string: one of "hidden" or "collapsed"). If "hidden", then the sidebar will not be visible. If "collapsed", the sidebar will be in a collapsed state initially but the user will be able to open it. Otherwise, by default, the sidebar is shown in an open and visible state. +- `footer`: (string: "false"). When set to "false", hides the Gradio footer. By default, the footer is visible. - `xmin`: (number) Set the initial minimum value for the x-axis limits across all metric plots. - `xmax`: (number) Set the initial maximum value for the x-axis limits across all metric plots. - `smoothing`: (number) Set the initial value of the smoothing slider (0-20, where 0 = no smoothing). diff --git a/docs/source/deploy_embed.md b/docs/source/deploy_embed.md index 62f0d36..28993ed 100644 --- a/docs/source/deploy_embed.md +++ b/docs/source/deploy_embed.md @@ -38,6 +38,7 @@ You can also filter the dashboard to display only specific projects or metrics u * `"hidden"` hides the sidebar completely. * `"collapsed"` keeps the sidebar initially collapsed, but the user can expand it. By default, the sidebar is visible and open. +* `footer` (string, `"false"`): When set to `"false"`, hides the Gradio footer. By default, the footer is visible. * `xmin` (number): Set the initial minimum value for the x-axis limits across all metrics plots. * `xmax` (number): Set the initial maximum value for the x-axis limits across all metrics plots. * `smoothing` (number): Set the initial value of the smoothing slider (0-20, where 0 = no smoothing). diff --git a/pyproject.toml b/pyproject.toml index 332c1ae..df75894 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ readme = "README.md" requires-python = ">=3.10" dependencies = [ "pandas<3.0.0", - "huggingface-hub<1.0.0", - "gradio[oauth]>=5.48.0,<6.0.0", + "huggingface-hub<2.0.0", + "gradio[oauth]>=6.0.0,<7.0.0", "numpy<3.0.0", "pillow<12.0.0", "orjson>=3.0,<4.0.0", diff --git a/tests/ui/test_ui_display.py b/tests/ui/test_ui_display.py index 57041df..2d27cb4 100644 --- a/tests/ui/test_ui_display.py +++ b/tests/ui/test_ui_display.py @@ -21,7 +21,7 @@ def test_that_runs_are_displayed(temp_dir): # The project name and run name should be displayed locator = page.get_by_label("Project") expect(locator).to_be_visible() - locator = page.get_by_text("test_run") + locator = page.get_by_test_id("checkbox-group").get_by_label("test_run") expect(locator).to_be_visible() # Initially, two line plots should be displayed @@ -29,7 +29,7 @@ def test_that_runs_are_displayed(temp_dir): expect(locator).to_have_count(2) # But if we uncheck the run, the line plots should be hidden - page.get_by_label("test_run").uncheck() + page.get_by_test_id("checkbox-group").get_by_label("test_run").uncheck() locator = page.locator(".vega-embed") expect(locator).to_have_count(0) diff --git a/trackio/__init__.py b/trackio/__init__.py index 713fc47..99c8329 100644 --- a/trackio/__init__.py +++ b/trackio/__init__.py @@ -1,4 +1,3 @@ -import hashlib import json import logging import os @@ -7,8 +6,6 @@ from pathlib import Path from typing import Any -from gradio.blocks import BUILT_IN_THEMES -from gradio.themes import Default as DefaultTheme from gradio.themes import ThemeClass from gradio.utils import TupleNoPrint from gradio_client import Client @@ -22,7 +19,7 @@ from trackio.run import Run from trackio.sqlite_storage import SQLiteStorage from trackio.table import Table -from trackio.ui.main import demo +from trackio.ui.main import CSS, HEAD, demo from trackio.utils import TRACKIO_DIR, TRACKIO_LOGO_DIR logging.getLogger("httpx").setLevel(logging.WARNING) @@ -144,7 +141,9 @@ def init( if url is None: if space_id is None: _, url, share_url = demo.launch( - show_api=False, + css=CSS, + head=HEAD, + footer_links=["gradio", "settings"], inline=False, quiet=True, prevent_thread_lock=True, @@ -173,7 +172,7 @@ def init( if utils.is_in_notebook() and embed: base_url = share_url + "/" if share_url else url full_url = utils.get_full_url( - base_url, project=project, write_token=demo.write_token + base_url, project=project, write_token=demo.write_token, footer=True ) utils.embed_url_in_notebook(full_url) else: @@ -311,10 +310,11 @@ def delete_project(project: str, force: bool = False) -> bool: def show( project: str | None = None, + *, theme: str | ThemeClass | None = None, mcp_server: bool | None = None, + footer: bool = True, color_palette: list[str] | None = None, - *, open_browser: bool = True, block_thread: bool | None = None, ): @@ -336,6 +336,9 @@ def show( functions will be added as MCP tools. If `None` (default behavior), then the `GRADIO_MCP_SERVER` environment variable will be used to determine if the MCP server should be enabled (which is `"True"` on Hugging Face Spaces). + footer (`bool`, *optional*, defaults to `True`): + Whether to show the Gradio footer. When `False`, the footer will be hidden. + This can also be controlled via the `footer` query parameter in the URL. color_palette (`list[str]`, *optional*): A list of hex color codes to use for plot lines. If not provided, the `TRACKIO_COLOR_PALETTE` environment variable will be used (comma-separated @@ -360,29 +363,6 @@ def show( theme = theme or os.environ.get("TRACKIO_THEME", DEFAULT_THEME) - if theme != DEFAULT_THEME: - # TODO: It's a little hacky to reproduce this theme-setting logic from Gradio Blocks, - # but in Gradio 6.0, the theme will be set in `launch()` instead, which means that we - # will be able to remove this code. - if isinstance(theme, str): - if theme.lower() in BUILT_IN_THEMES: - theme = BUILT_IN_THEMES[theme.lower()] - else: - try: - theme = ThemeClass.from_hub(theme) - except Exception as e: - warnings.warn(f"Cannot load {theme}. Caught Exception: {str(e)}") - theme = DefaultTheme() - if not isinstance(theme, ThemeClass): - warnings.warn("Theme should be a class loaded from gradio.themes") - theme = DefaultTheme() - demo.theme: ThemeClass = theme - demo.theme_css = theme._get_theme_css() - demo.stylesheets = theme._stylesheets - theme_hasher = hashlib.sha256() - theme_hasher.update(demo.theme_css.encode("utf-8")) - demo.theme_hash = theme_hasher.hexdigest() - _mcp_server = ( mcp_server if mcp_server is not None @@ -390,18 +370,21 @@ def show( ) app, url, share_url = demo.launch( - show_api=_mcp_server, + css=CSS, + head=HEAD, + footer_links=["gradio", "settings"] + (["api"] if _mcp_server else []), quiet=True, inline=False, prevent_thread_lock=True, favicon_path=TRACKIO_LOGO_DIR / "trackio_logo_light.png", allowed_paths=[TRACKIO_LOGO_DIR, TRACKIO_DIR], mcp_server=_mcp_server, + theme=theme, ) base_url = share_url + "/" if share_url else url full_url = utils.get_full_url( - base_url, project=project, write_token=demo.write_token + base_url, project=project, write_token=demo.write_token, footer=footer ) if not utils.is_in_notebook(): diff --git a/trackio/cli.py b/trackio/cli.py index cae24b3..edbbf06 100644 --- a/trackio/cli.py +++ b/trackio/cli.py @@ -24,6 +24,18 @@ def main(): action="store_true", help="Enable MCP server functionality. The Trackio dashboard will be set up as an MCP server and certain functions will be exposed as MCP tools.", ) + ui_parser.add_argument( + "--footer", + action="store_true", + default=True, + help="Show the Gradio footer. Use --no-footer to hide it.", + ) + ui_parser.add_argument( + "--no-footer", + dest="footer", + action="store_false", + help="Hide the Gradio footer.", + ) ui_parser.add_argument( "--color-palette", required=False, @@ -59,7 +71,13 @@ def main(): color_palette = None if args.color_palette: color_palette = [color.strip() for color in args.color_palette.split(",")] - show(args.project, args.theme, args.mcp_server, color_palette) + show( + project=args.project, + theme=args.theme, + mcp_server=args.mcp_server, + footer=args.footer, + color_palette=color_palette, + ) elif args.command == "sync": sync( project=args.project, diff --git a/trackio/ui/main.py b/trackio/ui/main.py index 68b8057..cf84475 100644 --- a/trackio/ui/main.py +++ b/trackio/ui/main.py @@ -558,7 +558,7 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): ) -css = """ +CSS = """ #run-cb .wrap { gap: 2px; } #run-cb .wrap label { line-height: 1; @@ -619,7 +619,7 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): } """ -javascript = """ +HEAD = """ """ @@ -667,7 +674,7 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): gr.set_static_paths(paths=[utils.MEDIA_DIR]) -with gr.Blocks(title="Trackio Dashboard", css=css, head=javascript) as demo: +with gr.Blocks(title="Trackio Dashboard") as demo: with gr.Sidebar(open=False) as sidebar: logo_urls = utils.get_logo_urls() logo = gr.Markdown( @@ -741,7 +748,7 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): smoothing_slider, ], queue=False, - api_name=False, + api_visibility="private", ) gr.on( [demo.load], @@ -749,7 +756,7 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): outputs=project_dd, show_progress="hidden", queue=False, - api_name=False, + api_visibility="private", ) gr.on( [timer.tick], @@ -757,14 +764,14 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): inputs=[project_dd, run_tb, run_selection_state, selected_runs_from_url], outputs=[run_cb, run_tb, run_selection_state], show_progress="hidden", - api_name=False, + api_visibility="private", ) gr.on( [timer.tick], fn=lambda: gr.Dropdown(info=fns.get_project_info()), outputs=[project_dd], show_progress="hidden", - api_name=False, + api_visibility="private", ) gr.on( [demo.load, project_dd.change], @@ -773,34 +780,34 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): outputs=[run_cb, run_tb, run_selection_state], show_progress="hidden", queue=False, - api_name=False, + api_visibility="private", ).then( fn=update_x_axis_choices, inputs=[project_dd, run_selection_state], outputs=x_axis_dd, show_progress="hidden", queue=False, - api_name=False, + api_visibility="private", ).then( fn=generate_embed, inputs=[project_dd, metric_filter_tb, run_selection_state], outputs=[embed_code], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ).then( fns.update_navbar_value, inputs=[project_dd], outputs=[navbar], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ).then( fn=fns.get_group_by_fields, inputs=[project_dd], outputs=[run_group_by_dd], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) @@ -811,7 +818,7 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): outputs=x_axis_dd, show_progress="hidden", queue=False, - api_name=False, + api_visibility="private", ) gr.on( [metric_filter_tb.change, run_cb.change], @@ -819,7 +826,7 @@ def create_media_section(media_by_run: dict[str, dict[str, list[MediaData]]]): inputs=[project_dd, metric_filter_tb, run_selection_state], outputs=embed_code, show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) @@ -835,7 +842,7 @@ def toggle_group_view(group_by_dd): inputs=[run_group_by_dd], outputs=[run_cb, grouped_runs_panel], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) @@ -843,28 +850,28 @@ def toggle_group_view(group_by_dd): fn=toggle_timer, inputs=realtime_cb, outputs=timer, - api_name=False, + api_visibility="private", queue=False, ) run_cb.input( fn=fns.handle_run_checkbox_change, inputs=[run_cb, run_selection_state], outputs=run_selection_state, - api_name=False, + api_visibility="private", queue=False, ).then( fn=generate_embed, inputs=[project_dd, metric_filter_tb, run_selection_state], outputs=embed_code, show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) run_tb.input( fn=refresh_runs, inputs=[project_dd, run_tb, run_selection_state], outputs=[run_cb, run_tb, run_selection_state], - api_name=False, + api_visibility="private", queue=False, show_progress="hidden", ) @@ -926,7 +933,7 @@ def update_last_steps(project): inputs=[project_dd], outputs=last_steps, show_progress="hidden", - api_name=False, + api_visibility="private", ) @gr.render( @@ -1076,13 +1083,13 @@ def update_dashboard( y_title=metric_name.split("/")[-1], color=color, color_map=color_map, + colors_in_legend=original_runs, title=metric_name, key=f"plot-{metric_idx}", preserved_by_key=None, + buttons=["fullscreen", "export"], x_lim=updated_x_lim, - show_fullscreen_button=True, min_width=400, - show_export_button=True, ) plot.select( update_x_lim, @@ -1141,13 +1148,13 @@ def update_dashboard( y_title=metric_name.split("/")[-1], color=color, color_map=color_map, + colors_in_legend=original_runs, title=metric_name, key=f"plot-{metric_idx}", preserved_by_key=None, + buttons=["fullscreen", "export"], x_lim=updated_x_lim, - show_fullscreen_button=True, min_width=400, - show_export_button=True, ) plot.select( update_x_lim, @@ -1410,7 +1417,7 @@ def render_grouped_runs(project, group_key, filter_text, selection): run_cb, ], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) @@ -1424,7 +1431,7 @@ def render_grouped_runs(project, group_key, filter_text, selection): ], outputs=[run_selection_state, group_cb, run_cb], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) diff --git a/trackio/ui/run_detail.py b/trackio/ui/run_detail.py index 1af74a1..6ee3d6c 100644 --- a/trackio/ui/run_detail.py +++ b/trackio/ui/run_detail.py @@ -73,13 +73,13 @@ def update_run_details(project, run): outputs=[project_dd, run_dd], show_progress="hidden", queue=False, - api_name=False, + api_visibility="private", ).then( fns.update_navbar_value, inputs=[project_dd], outputs=[navbar], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) @@ -89,6 +89,6 @@ def update_run_details(project, run): inputs=[project_dd, run_dd], outputs=[run_details, run_config], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) diff --git a/trackio/ui/runs.py b/trackio/ui/runs.py index fa988d7..9ca103e 100644 --- a/trackio/ui/runs.py +++ b/trackio/ui/runs.py @@ -200,14 +200,14 @@ def delete_selected_runs(deletion_allowed, runs_data, project, request: gr.Reque outputs=project_dd, show_progress="hidden", queue=False, - api_name=False, + api_visibility="private", ) gr.on( [timer.tick], fn=lambda: gr.Dropdown(info=fns.get_project_info()), outputs=[project_dd], show_progress="hidden", - api_name=False, + api_visibility="private", ) gr.on( [project_dd.change], @@ -215,14 +215,14 @@ def delete_selected_runs(deletion_allowed, runs_data, project, request: gr.Reque inputs=[project_dd], outputs=[runs_table], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ).then( fns.update_navbar_value, inputs=[project_dd], outputs=[navbar], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) @@ -232,7 +232,7 @@ def delete_selected_runs(deletion_allowed, runs_data, project, request: gr.Reque inputs=[], outputs=[delete_run_btn, runs_table, allow_deleting_runs], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) gr.on( @@ -241,7 +241,7 @@ def delete_selected_runs(deletion_allowed, runs_data, project, request: gr.Reque inputs=[allow_deleting_runs, runs_table], outputs=[delete_run_btn], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) gr.on( @@ -254,7 +254,7 @@ def delete_selected_runs(deletion_allowed, runs_data, project, request: gr.Reque inputs=None, outputs=[delete_run_btn, confirm_btn, cancel_btn], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) gr.on( @@ -267,7 +267,7 @@ def delete_selected_runs(deletion_allowed, runs_data, project, request: gr.Reque inputs=None, outputs=[delete_run_btn, confirm_btn, cancel_btn], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) gr.on( @@ -276,6 +276,6 @@ def delete_selected_runs(deletion_allowed, runs_data, project, request: gr.Reque inputs=[allow_deleting_runs, runs_table, project_dd], outputs=[runs_table], show_progress="hidden", - api_name=False, + api_visibility="private", queue=False, ) diff --git a/trackio/utils.py b/trackio/utils.py index 6a4396f..c65a34e 100644 --- a/trackio/utils.py +++ b/trackio/utils.py @@ -816,11 +816,15 @@ def deserialize_values(metrics): return result -def get_full_url(base_url: str, project: str | None, write_token: str) -> str: +def get_full_url( + base_url: str, project: str | None, write_token: str, footer: bool = True +) -> str: params = [] if project: params.append(f"project={project}") params.append(f"write_token={write_token}") + if not footer: + params.append("footer=false") return base_url + "?" + "&".join(params)