[Left nav refactor][6/6] Add tooltips for the sidebar icons when collapsed#20699
[Left nav refactor][6/6] Add tooltips for the sidebar icons when collapsed#20699daniellok-db merged 7 commits intomlflow:masterfrom
Conversation
52c8242 to
70105b1
Compare
🛠 DevTools 🛠
Install mlflow from this PRFor Databricks, use the following command: |
|
@daniellok-db Thank you for the contribution! Could you fix the following issue(s)? ⚠ DCO checkThe DCO check failed. Please sign off your commit(s) by following the instructions here. See https://github.com/mlflow/mlflow/blob/master/CONTRIBUTING.md#sign-your-work for more details. |
There was a problem hiding this comment.
Pull request overview
This PR is the final step in the left-nav refactor stack and focuses on improving usability of the collapsed sidebar by adding tooltips for icon-only navigation, along with related sidebar structure updates.
Changes:
- Introduces a reusable
MlflowSidebarLinkthat wraps sidebar links with tooltips when the sidebar is collapsed. - Refactors the main
MlflowSidebarto support a collapsed/expanded mode, nested experiment navigation, and nested gateway items. - Moves theme preference control into the Settings page (and removes the old header-based theme switch).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| mlflow/server/js/src/workspaces/components/WorkspaceSelector.tsx | Updates workspace selector behavior (adds clear handling + sizing tweaks). |
| mlflow/server/js/src/settings/SettingsPage.tsx | Adds a theme preference toggle and improves switch labels. |
| mlflow/server/js/src/lang/default/en.json | Updates extracted i18n strings to match new/updated UI text. |
| mlflow/server/js/src/common/utils/FeatureUtils.ts | Enables workflow-based navigation flag. |
| mlflow/server/js/src/common/components/MlflowSidebarWorkflowSwitch.tsx | New workflow “pill” switch UI in the sidebar. |
| mlflow/server/js/src/common/components/MlflowSidebarLink.tsx | New sidebar link wrapper with collapsed-mode tooltips + telemetry logging. |
| mlflow/server/js/src/common/components/MlflowSidebarGatewayItems.tsx | New nested gateway section items for the sidebar. |
| mlflow/server/js/src/common/components/MlflowSidebarExperimentItems.tsx | New nested experiment section items for the sidebar. |
| mlflow/server/js/src/common/components/MlflowSidebar.tsx | Refactors sidebar layout to support collapse + tooltips + nested sections. |
| mlflow/server/js/src/common/components/MlflowHeader.tsx | Removes the old header component. |
| mlflow/server/js/src/MlflowRouter.tsx | Removes header usage and always renders the sidebar component. |
| <MlflowSidebarLink | ||
| css={{ border: `1px solid ${theme.colors.actionDefaultBorderDefault}`, marginBottom: theme.spacing.sm }} | ||
| to={ExperimentTrackingRoutes.experimentsObservatoryRoute} | ||
| componentId="mlflow.experiment-sidebar.back-button" | ||
| isActive={isExperimentsActive} | ||
| onClick={onBackClick} | ||
| icon={<ArrowLeftIcon />} | ||
| collapsed={collapsed} | ||
| tooltipContent={ | ||
| <FormattedMessage defaultMessage="Back to experiment list" description="Tooltip for experiments button" /> | ||
| } | ||
| > | ||
| <BeakerIcon /> | ||
| {loading ? <Spinner /> : <Typography.Text ellipsis>{experiment?.name}</Typography.Text>} | ||
| </MlflowSidebarLink> |
There was a problem hiding this comment.
The back/experiment header link passes icon={<ArrowLeftIcon />} but also includes <BeakerIcon /> as the first child. Since MlflowSidebarLink already renders icon before children, this will render two icons in the expanded state; consider removing the extra icon from children (or moving it into the icon prop if that’s the intended leading icon).
| {Object.entries(config).map(([sectionKey, items]) => ( | ||
| <> | ||
| {sectionKey !== 'top-level' && collapsed ? ( | ||
| <div |
There was a problem hiding this comment.
Object.entries(config).map(...) returns a fragment (<>...</>) without a key, which will trigger React warnings and can cause unstable reconciliation. Use React.Fragment key={sectionKey} (or wrap in an element with key).
| logTelemetryEvent({ | ||
| componentId, | ||
| componentViewId: viewId, | ||
| componentType: DesignSystemEventProviderComponentTypes.Button, |
There was a problem hiding this comment.
Telemetry for this component is logged as DesignSystemEventProviderComponentTypes.Button, but the interactive element is a navigation Link. To keep analytics semantics consistent with other link telemetry in the codebase, this should likely be TypographyLink (or the appropriate link component type) instead of Button.
| componentType: DesignSystemEventProviderComponentTypes.Button, | |
| componentType: DesignSystemEventProviderComponentTypes.TypographyLink, |
| {showWorkspaceMenuItems && | ||
| menuItems.map( | ||
| ({ key, icon, linkProps, componentId, nestedItems }) => | ||
| nestedItems ?? ( | ||
| <MlflowSidebarLink | ||
| to={linkProps.to} | ||
| aria-current={linkProps.isActive(location) ? 'page' : undefined} | ||
| css={{ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: theme.spacing.sm, | ||
| color: theme.colors.textPrimary, | ||
| padding: `${theme.spacing.xs}px ${theme.spacing.sm}px`, | ||
| borderRadius: theme.borders.borderRadiusSm, | ||
| '&:hover': { | ||
| color: theme.colors.actionLinkHover, | ||
| backgroundColor: theme.colors.actionDefaultBackgroundHover, | ||
| }, | ||
| '&[aria-current="page"]': { | ||
| backgroundColor: theme.colors.actionDefaultBackgroundPress, | ||
| color: theme.isDarkMode ? theme.colors.blue300 : theme.colors.blue700, | ||
| fontWeight: theme.typography.typographyBoldFontWeight, | ||
| }, | ||
| }} | ||
| onClick={() => | ||
| logTelemetryEvent({ | ||
| componentId, | ||
| componentViewId: viewId, | ||
| componentType: DesignSystemEventProviderComponentTypes.TypographyLink, | ||
| componentSubType: null, | ||
| eventType: DesignSystemEventProviderAnalyticsEventTypes.OnClick, | ||
| }) | ||
| } | ||
| componentId={componentId} | ||
| isActive={linkProps.isActive} | ||
| icon={icon} | ||
| collapsed={!showSidebar} | ||
| > | ||
| {icon} | ||
| {linkProps.children} | ||
| </Link> | ||
| {nestedItemsGroups && nestedItemsGroups.length > 0 && ( | ||
| <ul css={NESTED_ITEMS_UL_CSS}> | ||
| {nestedItemsGroups.map((group) => ( | ||
| <Fragment key={group.sectionKey}> | ||
| {group.sectionKey !== 'top-level' && ( | ||
| <li | ||
| css={{ | ||
| display: 'flex', | ||
| marginTop: theme.spacing.xs, | ||
| marginBottom: theme.spacing.xs, | ||
| position: 'relative', | ||
| height: theme.typography.lineHeightBase, | ||
| paddingLeft: 40, | ||
| }} | ||
| > | ||
| <Typography.Text size="sm" color="secondary"> | ||
| {getExperimentPageSideNavSectionLabel(group.sectionKey, [])} | ||
| </Typography.Text> | ||
| </li> | ||
| )} | ||
| {group.items.map((nestedItem) => { | ||
| const isDisabled = !experimentId && key === 'experiments'; | ||
| return <li key={nestedItem.key}>{renderNestedItemLink(nestedItem, isDisabled)}</li>; | ||
| })} | ||
| </Fragment> | ||
| ))} | ||
| </ul> | ||
| )} | ||
| {nestedItems && nestedItems.length > 0 && ( | ||
| <ul css={NESTED_ITEMS_UL_CSS}> | ||
| {nestedItems.map((nestedItem) => ( | ||
| <li key={nestedItem.key}>{renderNestedItemLink(nestedItem, false)}</li> | ||
| ))} | ||
| </ul> | ||
| )} | ||
| </li> | ||
| ))} | ||
| </> | ||
| )} | ||
| </MlflowSidebarLink> | ||
| ), | ||
| )} |
There was a problem hiding this comment.
The <ul> here maps menuItems to elements without providing a key on the returned top-level element, which will cause React key warnings (keys inside MlflowSidebarLink don't help because they are not on the array elements). Also, nestedItems can be a <div>/fragment, which can lead to invalid <ul> children (only <li> is valid). Wrap each entry in a keyed <li> (or a keyed fragment) and ensure the rendered children under <ul> are list items.
| <div css={{ display: 'flex', flexDirection: 'column' }}> | ||
| <div | ||
| css={{ | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: theme.spacing.sm, | ||
| justifyContent: collapsed ? 'center' : 'flex-start', | ||
| paddingLeft: collapsed ? 0 : theme.spacing.sm, | ||
| paddingBlock: theme.spacing.xs, | ||
| border: collapsed ? `1px solid ${theme.colors.actionDefaultBorderDefault}` : 'none', | ||
| borderRadius: theme.borders.borderRadiusSm, | ||
| marginTop: collapsed ? theme.spacing.sm : 0, | ||
| marginBottom: collapsed ? theme.spacing.sm : 0, | ||
| }} | ||
| > | ||
| <CloudModelIcon /> | ||
| {!collapsed && <FormattedMessage defaultMessage="AI Gateway" description="Sidebar link for gateway" />} | ||
| </div> | ||
| <MlflowSidebarLink | ||
| css={{ paddingLeft: collapsed ? undefined : theme.spacing.lg }} | ||
| to={GatewayRoutes.gatewayPageRoute} | ||
| componentId="mlflow.sidebar.gateway_endpoints_tab_link" | ||
| isActive={isEndpointsActive} | ||
| icon={<ChainIcon />} | ||
| collapsed={collapsed} | ||
| > | ||
| <FormattedMessage defaultMessage="Endpoints" description="Sidebar link for gateway endpoints" /> | ||
| </MlflowSidebarLink> | ||
| <MlflowSidebarLink | ||
| css={{ paddingLeft: collapsed ? undefined : theme.spacing.lg }} | ||
| to={GatewayRoutes.apiKeysPageRoute} | ||
| componentId="mlflow.sidebar.gateway_api_keys_tab_link" | ||
| isActive={isApiKeysActive} | ||
| icon={<KeyIcon />} | ||
| collapsed={collapsed} | ||
| > | ||
| <FormattedMessage defaultMessage="API Keys" description="Sidebar link for gateway API keys" /> | ||
| </MlflowSidebarLink> | ||
| </div> |
There was a problem hiding this comment.
MlflowSidebarGatewayItems renders a top-level <div> (and nested <div> header) which will be inserted directly under the sidebar’s <ul> when nestedItems is used. That produces invalid list markup and can hurt accessibility/styling. Consider returning <li> elements (or wrapping the whole block in a single <li> containing an inner <ul>), and add an accessible label/tooltip for the collapsed header icon since it otherwise has no text.
| <div css={{ display: 'flex', flexDirection: 'column' }}> | |
| <div | |
| css={{ | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: theme.spacing.sm, | |
| justifyContent: collapsed ? 'center' : 'flex-start', | |
| paddingLeft: collapsed ? 0 : theme.spacing.sm, | |
| paddingBlock: theme.spacing.xs, | |
| border: collapsed ? `1px solid ${theme.colors.actionDefaultBorderDefault}` : 'none', | |
| borderRadius: theme.borders.borderRadiusSm, | |
| marginTop: collapsed ? theme.spacing.sm : 0, | |
| marginBottom: collapsed ? theme.spacing.sm : 0, | |
| }} | |
| > | |
| <CloudModelIcon /> | |
| {!collapsed && <FormattedMessage defaultMessage="AI Gateway" description="Sidebar link for gateway" />} | |
| </div> | |
| <MlflowSidebarLink | |
| css={{ paddingLeft: collapsed ? undefined : theme.spacing.lg }} | |
| to={GatewayRoutes.gatewayPageRoute} | |
| componentId="mlflow.sidebar.gateway_endpoints_tab_link" | |
| isActive={isEndpointsActive} | |
| icon={<ChainIcon />} | |
| collapsed={collapsed} | |
| > | |
| <FormattedMessage defaultMessage="Endpoints" description="Sidebar link for gateway endpoints" /> | |
| </MlflowSidebarLink> | |
| <MlflowSidebarLink | |
| css={{ paddingLeft: collapsed ? undefined : theme.spacing.lg }} | |
| to={GatewayRoutes.apiKeysPageRoute} | |
| componentId="mlflow.sidebar.gateway_api_keys_tab_link" | |
| isActive={isApiKeysActive} | |
| icon={<KeyIcon />} | |
| collapsed={collapsed} | |
| > | |
| <FormattedMessage defaultMessage="API Keys" description="Sidebar link for gateway API keys" /> | |
| </MlflowSidebarLink> | |
| </div> | |
| <FormattedMessage defaultMessage="AI Gateway" description="Sidebar link for gateway"> | |
| {(gatewayLabel) => ( | |
| <li css={{ display: 'flex', flexDirection: 'column' }}> | |
| <div | |
| css={{ | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: theme.spacing.sm, | |
| justifyContent: collapsed ? 'center' : 'flex-start', | |
| paddingLeft: collapsed ? 0 : theme.spacing.sm, | |
| paddingBlock: theme.spacing.xs, | |
| border: collapsed ? `1px solid ${theme.colors.actionDefaultBorderDefault}` : 'none', | |
| borderRadius: theme.borders.borderRadiusSm, | |
| marginTop: collapsed ? theme.spacing.sm : 0, | |
| marginBottom: collapsed ? theme.spacing.sm : 0, | |
| }} | |
| aria-label={collapsed ? String(gatewayLabel) : undefined} | |
| title={String(gatewayLabel)} | |
| > | |
| <CloudModelIcon aria-hidden="true" /> | |
| {!collapsed && gatewayLabel} | |
| </div> | |
| <MlflowSidebarLink | |
| css={{ paddingLeft: collapsed ? undefined : theme.spacing.lg }} | |
| to={GatewayRoutes.gatewayPageRoute} | |
| componentId="mlflow.sidebar.gateway_endpoints_tab_link" | |
| isActive={isEndpointsActive} | |
| icon={<ChainIcon />} | |
| collapsed={collapsed} | |
| > | |
| <FormattedMessage defaultMessage="Endpoints" description="Sidebar link for gateway endpoints" /> | |
| </MlflowSidebarLink> | |
| <MlflowSidebarLink | |
| css={{ paddingLeft: collapsed ? undefined : theme.spacing.lg }} | |
| to={GatewayRoutes.apiKeysPageRoute} | |
| componentId="mlflow.sidebar.gateway_api_keys_tab_link" | |
| isActive={isApiKeysActive} | |
| icon={<KeyIcon />} | |
| collapsed={collapsed} | |
| > | |
| <FormattedMessage defaultMessage="API Keys" description="Sidebar link for gateway API keys" /> | |
| </MlflowSidebarLink> | |
| </li> | |
| )} | |
| </FormattedMessage> |
| <> | ||
| <div | ||
| css={{ | ||
| borderTop: `1px solid ${theme.colors.actionDefaultBorderDefault}`, | ||
| width: '100%', | ||
| paddingBottom: theme.spacing.sm, | ||
| marginTop: theme.spacing.xs, | ||
| }} | ||
| /> |
There was a problem hiding this comment.
This component returns a fragment containing <div> separators and <li> elements but does not provide an enclosing <ul>/<ol>. When it’s rendered as nestedItems inside the main sidebar <ul>, the <div>s become invalid direct children of a <ul>. Consider restructuring so that everything under the sidebar’s <ul> is <li> (e.g., wrap the whole nested block in a single <li> containing its own inner <ul>).
| <div | ||
| css={{ | ||
| position: 'relative', | ||
| background: borderColor, | ||
| margin: -1, | ||
| borderRadius: theme.borders.borderRadiusFull, | ||
| padding: 1, | ||
| cursor: 'pointer', | ||
| }} | ||
| onClick={() => setWorkflowType(workflowType)} | ||
| > |
There was a problem hiding this comment.
Pill is implemented as a clickable <div> without role, tabIndex, or keyboard handlers, so it’s not keyboard-accessible and won’t be announced correctly by assistive tech. Use a semantic <button> (preferred) or add role="button", tabIndex={0}, and handle Enter/Space, plus appropriate aria-pressed for the active state.
| return ( | ||
| <li key={componentId}> | ||
| <Tooltip |
There was a problem hiding this comment.
MlflowSidebarLink renders a <li> internally. This makes the component invalid to use outside of a <ul>/<ol> (e.g., the Docs/GitHub/Settings links are rendered inside a <div> in MlflowSidebar). Consider removing the <li> wrapper from MlflowSidebarLink and letting call sites wrap it in <li> only where appropriate (or add an asListItem/wrapper option).
| <Tooltip | ||
| componentId="mlflow.sidebar.assistant_tooltip" | ||
| content={<FormattedMessage defaultMessage="Assistant" description="Tooltip for assistant button" />} | ||
| open={isAssistantHovered && !showSidebar} |
There was a problem hiding this comment.
This tooltip is controlled via open={isAssistantHovered && !showSidebar}, so it won’t appear on keyboard focus when the sidebar is collapsed (and it also duplicates the design-system’s own hover/focus handling). Consider using open={!showSidebar ? undefined : false} (and letting the tooltip handle focus/hover) so collapsed-icon tooltips work for keyboard users too.
| open={isAssistantHovered && !showSidebar} | |
| open={!showSidebar ? undefined : false} |
|
Documentation preview for 3cdf599 is available at: More info
|
d09f5e5 to
739e88b
Compare
ffddb47 to
9115a35
Compare
9115a35 to
5f1c571
Compare
5f1c571 to
3cdf599
Compare
🥞 Stacked PR
Use this link to review incremental changes.
Related Issues/PRs
#xxxWhat changes are proposed in this pull request?
As title, i missed this previously but when the sidebar is collapsed it's hard to know what the icons mean. This PR adds tooltips.
How is this PR tested?
Screen.Recording.2026-02-11.at.2.00.27.PM.mov
With workspace mode enabled (currently genai / ML preference is not scoped to workspace, but this can be followed up between RC and stable):
Screen.Recording.2026-02-11.at.2.08.53.PM.mov
Does this PR require documentation update?
Does this PR require updating the MLflow Skills repository?
Release Notes
Is this a user-facing change?
What component(s), interfaces, languages, and integrations does this PR affect?
Components
area/tracking: Tracking Service, tracking client APIs, autologgingarea/models: MLmodel format, model serialization/deserialization, flavorsarea/model-registry: Model Registry service, APIs, and the fluent client calls for Model Registryarea/scoring: MLflow Model server, model deployment tools, Spark UDFsarea/evaluation: MLflow model evaluation features, evaluation metrics, and evaluation workflowsarea/gateway: MLflow AI Gateway client APIs, server, and third-party integrationsarea/prompts: MLflow prompt engineering features, prompt templates, and prompt managementarea/tracing: MLflow Tracing features, tracing APIs, and LLM tracing functionalityarea/projects: MLproject format, project running backendsarea/uiux: Front-end, user experience, plotting, JavaScript, JavaScript dev serverarea/build: Build and test infrastructure for MLflowarea/docs: MLflow documentation pagesHow should the PR be classified in the release notes? Choose one:
rn/none- No description will be included. The PR will be mentioned only by the PR number in the "Small Bugfixes and Documentation Updates" sectionrn/breaking-change- The PR will be mentioned in the "Breaking Changes" sectionrn/feature- A new user-facing feature worth mentioning in the release notesrn/bug-fix- A user-facing bug fix worth mentioning in the release notesrn/documentation- A user-facing documentation change worth mentioning in the release notesShould this PR be included in the next patch release?
Yesshould be selected for bug fixes, documentation updates, and other small changes.Noshould be selected for new features and larger changes. If you're unsure about the release classification of this PR, leave this unchecked to let the maintainers decide.What is a minor/patch release?
Bug fixes, doc updates and new features usually go into minor releases.
Bug fixes and doc updates usually go into patch releases.