You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2025/12/18 08:45:35 UTC
(superset) 02/06: feat(dashboard): add ComponentHeaderControls for consistent component menus
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch feat/granular-theming
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 737c39a45e0a247824ac55f46c4112c988d4855e
Author: Evan Rusackas <ev...@rusackas.com>
AuthorDate: Mon Dec 15 16:50:34 2025 -0800
feat(dashboard): add ComponentHeaderControls for consistent component menus
Create a generic menu component for dashboard components (Markdown, Row,
Column, Tab) that provides a consistent vertical dots menu pattern,
matching the existing SliceHeaderControls used for charts.
New files:
- ComponentHeaderControls/index.tsx: Main component with:
- NoAnimationDropdown + Menu pattern from @superset-ui/core
- ComponentMenuKeys enum for standard actions (Delete, ApplyTheme, etc.)
- Configurable visibility (editMode, showInViewMode)
- ComponentHeaderControls/useComponentMenuItems.tsx: Helper hook that:
- Builds standard menu items (theme selection, delete)
- Supports custom items before/after standard items
- Shows applied theme name in menu
This is the foundation for adding granular theming to all dashboard
components with a consistent UI pattern.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <no...@anthropic.com>
---
docs/GRANULAR_THEMING_PLAN.md | 19 ++
.../menu/ComponentHeaderControls/index.tsx | 193 +++++++++++++++++++++
.../useComponentMenuItems.tsx | 130 ++++++++++++++
3 files changed, 342 insertions(+)
diff --git a/docs/GRANULAR_THEMING_PLAN.md b/docs/GRANULAR_THEMING_PLAN.md
index 53d76df156..47b0d5e09c 100644
--- a/docs/GRANULAR_THEMING_PLAN.md
+++ b/docs/GRANULAR_THEMING_PLAN.md
@@ -344,3 +344,22 @@ _Ongoing notes as we implement..._
- Decided on modal with live preview for theme selection
- Clarified color scheme (data) vs theme (UI) separation
+### Session 1 - Implementation Started
+- Created `ComponentHeaderControls` component at:
+ `src/dashboard/components/menu/ComponentHeaderControls/index.tsx`
+ - Generic vertical dots menu matching SliceHeaderControls pattern
+ - Uses NoAnimationDropdown + Menu from @superset-ui/core
+ - Configurable menu items, edit mode visibility
+ - Exports `ComponentMenuKeys` enum for standard actions
+
+- Created `useComponentMenuItems` hook at:
+ `src/dashboard/components/menu/ComponentHeaderControls/useComponentMenuItems.tsx`
+ - Builds standard menu items (theme, delete)
+ - Supports custom items before/after standard items
+ - Shows "Change theme (name)" when theme applied
+
+**Next Steps:**
+1. Integrate ComponentHeaderControls into Markdown component
+2. Test with simple Edit/Preview + Theme + Delete menu
+3. Remove old MarkdownModeDropdown
+
diff --git a/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/index.tsx
new file mode 100644
index 0000000000..0eb91d1fd8
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/index.tsx
@@ -0,0 +1,193 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useState, Key, MouseEvent, KeyboardEvent } from 'react';
+import { t } from '@superset-ui/core';
+import { css, useTheme } from '@apache-superset/core/ui';
+import { Menu, MenuItem } from '@superset-ui/core/components/Menu';
+import {
+ NoAnimationDropdown,
+ Button,
+} from '@superset-ui/core/components';
+import { Icons } from '@superset-ui/core/components/Icons';
+
+/**
+ * Standard menu keys for dashboard components.
+ * Components can use these standard keys or define custom ones.
+ */
+export enum ComponentMenuKeys {
+ // Common actions
+ Delete = 'delete',
+ Edit = 'edit',
+
+ // Theme actions
+ ApplyTheme = 'apply-theme',
+ ClearTheme = 'clear-theme',
+
+ // Markdown-specific
+ EditContent = 'edit-content',
+ PreviewContent = 'preview-content',
+
+ // Row/Column-specific
+ BackgroundStyle = 'background-style',
+
+ // Tab-specific
+ RenameTab = 'rename-tab',
+}
+
+// Re-export MenuItem type for convenience - allows both keyed items and dividers
+export type ComponentMenuItem = MenuItem;
+
+export interface ComponentHeaderControlsProps {
+ /** Unique identifier for the component */
+ componentId: string;
+
+ /** Array of menu items to display */
+ menuItems: ComponentMenuItem[];
+
+ /** Callback when a menu item is clicked */
+ onMenuClick: (key: string, domEvent: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>) => void;
+
+ /** Whether the component is in edit mode */
+ editMode?: boolean;
+
+ /** Whether to show the menu even in view mode */
+ showInViewMode?: boolean;
+
+ /** Z-index for the dropdown overlay */
+ zIndex?: number;
+
+ /** Additional CSS class for the trigger button */
+ className?: string;
+
+ /** Whether the menu is disabled */
+ disabled?: boolean;
+}
+
+const VerticalDotsTrigger = () => {
+ const theme = useTheme();
+ return (
+ <Icons.EllipsisOutlined
+ css={css`
+ transform: rotate(90deg);
+ &:hover {
+ cursor: pointer;
+ }
+ `}
+ iconSize="l"
+ iconColor={theme.colorTextLabel}
+ className="component-menu-trigger"
+ />
+ );
+};
+
+/**
+ * A standardized menu component for dashboard components (Markdown, Row, Column, Tab).
+ *
+ * Provides a consistent vertical dots menu pattern similar to SliceHeaderControls,
+ * but generic enough to be used across all dashboard component types.
+ *
+ * Usage:
+ * ```tsx
+ * <ComponentHeaderControls
+ * componentId="MARKDOWN-123"
+ * menuItems={[
+ * { key: ComponentMenuKeys.Edit, label: t('Edit') },
+ * { key: ComponentMenuKeys.ApplyTheme, label: t('Apply theme') },
+ * { type: 'divider' },
+ * { key: ComponentMenuKeys.Delete, label: t('Delete'), danger: true },
+ * ]}
+ * onMenuClick={(key) => handleMenuAction(key)}
+ * editMode={editMode}
+ * />
+ * ```
+ */
+const ComponentHeaderControls = ({
+ componentId,
+ menuItems,
+ onMenuClick,
+ editMode = false,
+ showInViewMode = false,
+ zIndex = 99,
+ className,
+ disabled = false,
+}: ComponentHeaderControlsProps) => {
+ const [isDropdownVisible, setIsDropdownVisible] = useState(false);
+ const theme = useTheme();
+
+ // Don't render if not in edit mode and showInViewMode is false
+ if (!editMode && !showInViewMode) {
+ return null;
+ }
+
+ const handleMenuClick = ({
+ key,
+ domEvent,
+ }: {
+ key: Key;
+ domEvent: MouseEvent<HTMLElement> | KeyboardEvent<HTMLElement>;
+ }) => {
+ onMenuClick(String(key), domEvent);
+ setIsDropdownVisible(false);
+ };
+
+ const dropdownOverlayStyle = {
+ zIndex,
+ animationDuration: '0s',
+ };
+
+ return (
+ <NoAnimationDropdown
+ popupRender={() => (
+ <Menu
+ onClick={handleMenuClick}
+ data-test={`component-menu-${componentId}`}
+ id={`component-menu-${componentId}`}
+ selectable={false}
+ items={menuItems}
+ />
+ )}
+ overlayStyle={dropdownOverlayStyle}
+ trigger={['click']}
+ placement="bottomRight"
+ open={isDropdownVisible}
+ onOpenChange={visible => setIsDropdownVisible(visible)}
+ disabled={disabled}
+ >
+ <Button
+ id={`${componentId}-controls`}
+ buttonStyle="link"
+ aria-label={t('More Options')}
+ aria-haspopup="true"
+ className={className}
+ css={css`
+ padding: ${theme.sizeUnit}px;
+ opacity: 0.7;
+ transition: opacity 0.2s;
+ &:hover {
+ opacity: 1;
+ }
+ `}
+ >
+ <VerticalDotsTrigger />
+ </Button>
+ </NoAnimationDropdown>
+ );
+};
+
+export default ComponentHeaderControls;
diff --git a/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/useComponentMenuItems.tsx b/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/useComponentMenuItems.tsx
new file mode 100644
index 0000000000..de2acfde79
--- /dev/null
+++ b/superset-frontend/src/dashboard/components/menu/ComponentHeaderControls/useComponentMenuItems.tsx
@@ -0,0 +1,130 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { useMemo } from 'react';
+import { t } from '@superset-ui/core';
+import { Icons } from '@superset-ui/core/components/Icons';
+import { css } from '@apache-superset/core/ui';
+import { MenuItem } from '@superset-ui/core/components/Menu';
+import { ComponentMenuKeys } from './index';
+
+const dropdownIconsStyles = css`
+ &&.anticon > .anticon:first-child {
+ margin-right: 0;
+ vertical-align: 0;
+ }
+`;
+
+export interface UseComponentMenuItemsOptions {
+ /** Whether to include theme menu item */
+ includeTheme?: boolean;
+
+ /** Whether to include delete menu item */
+ includeDelete?: boolean;
+
+ /** Whether the component has a theme applied */
+ hasThemeApplied?: boolean;
+
+ /** Name of the applied theme (for display) */
+ appliedThemeName?: string;
+
+ /** Additional custom menu items to include before standard items */
+ customItems?: MenuItem[];
+
+ /** Additional custom menu items to include after standard items */
+ customItemsAfter?: MenuItem[];
+}
+
+/**
+ * Hook to build standard menu items for dashboard components.
+ *
+ * Provides consistent menu item structure across all component types,
+ * with optional theme selection and delete actions.
+ */
+export function useComponentMenuItems({
+ includeTheme = true,
+ includeDelete = true,
+ hasThemeApplied = false,
+ appliedThemeName,
+ customItems = [],
+ customItemsAfter = [],
+}: UseComponentMenuItemsOptions = {}): MenuItem[] {
+ return useMemo(() => {
+ const items: MenuItem[] = [];
+
+ // Add custom items first
+ if (customItems.length > 0) {
+ items.push(...customItems);
+ }
+
+ // Add theme items
+ if (includeTheme) {
+ if (items.length > 0) {
+ items.push({ type: 'divider' });
+ }
+
+ items.push({
+ key: ComponentMenuKeys.ApplyTheme,
+ label: hasThemeApplied
+ ? t('Change theme (%s)', appliedThemeName || t('Custom'))
+ : t('Apply theme'),
+ icon: <Icons.BgColorsOutlined css={dropdownIconsStyles} />,
+ });
+
+ if (hasThemeApplied) {
+ items.push({
+ key: ComponentMenuKeys.ClearTheme,
+ label: t('Clear theme'),
+ icon: <Icons.ClearOutlined css={dropdownIconsStyles} />,
+ });
+ }
+ }
+
+ // Add custom items after theme
+ if (customItemsAfter.length > 0) {
+ if (items.length > 0) {
+ items.push({ type: 'divider' });
+ }
+ items.push(...customItemsAfter);
+ }
+
+ // Add delete as last item
+ if (includeDelete) {
+ if (items.length > 0) {
+ items.push({ type: 'divider' });
+ }
+ items.push({
+ key: ComponentMenuKeys.Delete,
+ label: t('Delete'),
+ icon: <Icons.DeleteOutlined css={dropdownIconsStyles} />,
+ danger: true,
+ });
+ }
+
+ return items;
+ }, [
+ includeTheme,
+ includeDelete,
+ hasThemeApplied,
+ appliedThemeName,
+ customItems,
+ customItemsAfter,
+ ]);
+}
+
+export default useComponentMenuItems;