This is an automated email from the ASF dual-hosted git repository. rusackas pushed a commit to branch mobile-dashboard-support in repository https://gitbox.apache.org/repos/asf/superset.git
commit a8a6d5f37aaf2e7447a8fc78ad913711c254093b Author: Evan Rusackas <[email protected]> AuthorDate: Fri Jan 9 17:15:21 2026 -0800 feat(mobile): Add filter drawer and chart consumption mode for mobile dashboards - Add left-side filter drawer with vertical filter layout on mobile - Hide Actions header and show Apply/Clear buttons side by side - Add filter button to dashboard header (only when filters exist) - Support leftPanelItems prop in PageHeaderWithActions - Hide chart kebab menu and disable title links on mobile - Show full chart titles without truncation on mobile - Center dashboard title on mobile with filter icon on left 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --- .../src/components/PageHeaderWithActions/index.tsx | 12 +++ .../DashboardBuilder/DashboardBuilder.tsx | 102 ++++++++++++++++----- .../DashboardBuilder/DashboardWrapper.tsx | 23 +++++ .../dashboard/components/DashboardBuilder/state.ts | 1 + .../src/dashboard/components/Header/index.jsx | 26 +++++- 5 files changed, 140 insertions(+), 24 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/components/PageHeaderWithActions/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/PageHeaderWithActions/index.tsx index cd523ea6ea..4c48594c63 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/PageHeaderWithActions/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/PageHeaderWithActions/index.tsx @@ -81,6 +81,15 @@ const headerStyles = (theme: SupersetTheme) => css` display: flex; align-items: center; } + + /* Mobile: center the title between left and right panels */ + @media (max-width: 767px) { + .title-panel { + flex: 1; + justify-content: center; + margin-right: 0; + } + } `; const buttonsStyles = (theme: SupersetTheme) => css` @@ -108,6 +117,7 @@ export type PageHeaderWithActionsProps = { showFaveStar: boolean; showMenuDropdown?: boolean; faveStarProps: FaveStarProps; + leftPanelItems?: ReactNode; titlePanelAdditionalItems: ReactNode; rightPanelAdditionalItems: ReactNode; additionalActionsMenu: ReactElement; @@ -124,6 +134,7 @@ export const PageHeaderWithActions = ({ certificatiedBadgeProps, showFaveStar, faveStarProps, + leftPanelItems, titlePanelAdditionalItems, rightPanelAdditionalItems, additionalActionsMenu, @@ -134,6 +145,7 @@ export const PageHeaderWithActions = ({ const theme = useTheme(); return ( <div css={headerStyles} className="header-with-actions"> + {leftPanelItems} <div className="title-panel"> <DynamicEditableTitle {...editableTitleProps} /> {showTitlePanelItems && ( diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index 82721d386d..8a99960a6a 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -461,6 +461,7 @@ const DashboardBuilder = () => { dashboardFiltersOpen, toggleDashboardFiltersOpen, nativeFiltersEnabled, + hasFilters, } = useNativeFilters(); const [containerRef, isSticky] = useElementOnScreen<HTMLDivElement>( @@ -520,24 +521,14 @@ const DashboardBuilder = () => { const renderDraggableContent = useCallback( ({ dropIndicatorProps }: { dropIndicatorProps: JsonObject }) => ( <div> - {!hideDashboardHeader && <DashboardHeader />} - {/* Mobile filter button */} - {!isNotMobile && !editMode && nativeFiltersEnabled && ( - <div - css={css` - padding: ${theme.sizeUnit * 2}px ${theme.sizeUnit * 4}px; - background: ${theme.colorBgBase}; - border-bottom: 1px solid ${theme.colorBorderSecondary}; - `} - > - <Button - buttonStyle="secondary" - onClick={() => setMobileFiltersOpen(true)} - > - <Icons.FilterOutlined iconSize="m" /> - {t('Filters')} - </Button> - </div> + {!hideDashboardHeader && ( + <DashboardHeader + onOpenMobileFilters={ + !isNotMobile && nativeFiltersEnabled && hasFilters + ? () => setMobileFiltersOpen(true) + : undefined + } + /> )} {showFilterBar && filterBarOrientation === FilterBarOrientation.Horizontal && ( @@ -576,6 +567,7 @@ const DashboardBuilder = () => { ), [ nativeFiltersEnabled, + hasFilters, filterBarOrientation, editMode, handleChangeTab, @@ -752,13 +744,81 @@ const DashboardBuilder = () => { {!isNotMobile && nativeFiltersEnabled && ( <Drawer title={t('Filters')} - placement="bottom" + placement="left" onClose={() => setMobileFiltersOpen(false)} open={mobileFiltersOpen} - height="70vh" + width="85vw" + styles={{ + body: { + padding: 0, + display: 'flex', + flexDirection: 'column', + }, + }} + css={css` + /* Mobile filter drawer overrides */ + + /* Hide the Header component (contains Actions title, settings, collapse button) */ + /* Target the parent div that contains the collapse button using :has() */ + div:has([data-test='filter-bar-collapse-button']) { + display: none !important; + } + + /* Hide the collapsed bar */ + [data-test='filter-bar-collapsable'] { + display: none !important; + } + + /* Action buttons: side by side, not fixed position */ + [data-test='filterbar-action-buttons'] { + position: relative !important; + flex-direction: row !important; + width: 100% !important; + padding: ${theme.sizeUnit * 4}px !important; + background: ${theme.colorBgContainer} !important; + border-top: 1px solid ${theme.colorBorderSecondary} !important; + gap: ${theme.sizeUnit * 2}px !important; + bottom: auto !important; + left: auto !important; + + .filter-apply-button { + margin-bottom: 0 !important; + flex: 1; + } + .filter-clear-all-button { + flex: 1; + } + } + + /* Remove border-right and make full width */ + [data-test='filter-bar'] { + position: relative; + width: 100% !important; + height: 100%; + border-right: none; + + & > .open { + position: relative; + width: 100% !important; + height: 100%; + min-height: 100%; + border-right: none !important; + border-bottom: none !important; + display: flex; + flex-direction: column; + } + } + `} > <FilterBar - orientation={FilterBarOrientation.Horizontal} + orientation={FilterBarOrientation.Vertical} + verticalConfig={{ + filtersOpen: true, + toggleFiltersBar: () => {}, + width: 300, + height: '100%', + offset: 0, + }} hidden={false} /> </Drawer> diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx index ec423b7bb0..bb8f2b08b7 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx @@ -110,6 +110,29 @@ const StyledDiv = styled.div` i.warning { color: ${theme.colorWarning}; } + + /* Mobile: consumption-only mode */ + @media (max-width: 767px) { + /* Hide chart kebab menu (SliceHeaderControls) */ + [data-test='slice-header'] .header-controls [id$='-controls'] { + display: none; + } + + /* Disable chart title links - make them plain text */ + [data-test='slice-header'] .header-title a { + pointer-events: none; + text-decoration: none !important; + } + + /* Show full chart title without truncation - no tooltip needed */ + [data-test='slice-header'] .header-title { + -webkit-line-clamp: unset; + display: block; + white-space: normal; + overflow: visible; + text-overflow: unset; + } + } `} `; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts index c7b7a56fdf..028283d22d 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/state.ts @@ -102,5 +102,6 @@ export const useNativeFilters = () => { dashboardFiltersOpen, toggleDashboardFiltersOpen, nativeFiltersEnabled, + hasFilters: filterValues.length > 0, }; }; diff --git a/superset-frontend/src/dashboard/components/Header/index.jsx b/superset-frontend/src/dashboard/components/Header/index.jsx index 9ee450bfe6..86242a1387 100644 --- a/superset-frontend/src/dashboard/components/Header/index.jsx +++ b/superset-frontend/src/dashboard/components/Header/index.jsx @@ -25,7 +25,7 @@ import { t, getExtensionsRegistry, } from '@superset-ui/core'; -import { styled, css } from '@apache-superset/core/ui'; +import { styled, css, useTheme } from '@apache-superset/core/ui'; import { Global } from '@emotion/react'; import { shallowEqual, useDispatch, useSelector } from 'react-redux'; import { bindActionCreators } from 'redux'; @@ -60,7 +60,10 @@ import setPeriodicRunner, { } from 'src/dashboard/util/setPeriodicRunner'; import ReportModal from 'src/features/reports/ReportModal'; import { deleteActiveReport } from 'src/features/reports/ReportModal/actions'; -import { PageHeaderWithActions } from '@superset-ui/core/components/PageHeaderWithActions'; +import { + PageHeaderWithActions, + menuTriggerStyles, +} from '@superset-ui/core/components/PageHeaderWithActions'; import { useUnsavedChangesPrompt } from 'src/hooks/useUnsavedChangesPrompt'; import DashboardEmbedModal from '../EmbeddedModal'; import OverwriteConfirm from '../OverwriteConfirm'; @@ -166,8 +169,9 @@ const discardChanges = () => { const { useBreakpoint } = Grid; -const Header = () => { +const Header = ({ onOpenMobileFilters }) => { const dispatch = useDispatch(); + const theme = useTheme(); const screens = useBreakpoint(); const isMobile = !screens.md; const [didNotifyMaxUndoHistoryToast, setDidNotifyMaxUndoHistoryToast] = @@ -810,6 +814,22 @@ const Header = () => { editableTitleProps={editableTitleProps} certificatiedBadgeProps={certifiedBadgeProps} faveStarProps={faveStarProps} + leftPanelItems={ + onOpenMobileFilters && ( + <Button + css={menuTriggerStyles} + buttonStyle="tertiary" + aria-label={t('Open filters')} + onClick={onOpenMobileFilters} + data-test="mobile-filters-trigger" + > + <Icons.FilterOutlined + iconColor={theme.colorPrimary} + iconSize="l" + /> + </Button> + ) + } titlePanelAdditionalItems={titlePanelAdditionalItems} rightPanelAdditionalItems={rightPanelAdditionalItems} menuDropdownProps={{
