diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton/ExploreResultsButton.test.tsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton/ExploreResultsButton.test.tsx index 63283a08625..9a00dda136e 100644 --- a/superset-frontend/src/SqlLab/components/ExploreResultsButton/ExploreResultsButton.test.tsx +++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton/ExploreResultsButton.test.tsx @@ -34,18 +34,14 @@ const setup = ( // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('ExploreResultsButton', () => { test('renders', async () => { - const { queryByText } = setup(jest.fn(), { + setup(jest.fn(), { database: { allows_subquery: true }, }); - expect(queryByText('Create chart')).toBeInTheDocument(); - // Updated line to match the actual button name that includes the icon expect(screen.getByRole('button', { name: /Create chart/i })).toBeEnabled(); }); test('renders disabled if subquery not allowed', async () => { - const { queryByText } = setup(jest.fn()); - expect(queryByText('Create chart')).toBeInTheDocument(); - // Updated line to match the actual button name that includes the icon + setup(jest.fn()); expect( screen.getByRole('button', { name: /Create chart/i }), ).toBeDisabled(); diff --git a/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx b/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx index c4853025106..39deb485b2c 100644 --- a/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx +++ b/superset-frontend/src/SqlLab/components/ExploreResultsButton/index.tsx @@ -38,16 +38,16 @@ const ExploreResultsButton = ({ return ( + /> ); }; diff --git a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx index d3e37a3586d..b8c85f06284 100644 --- a/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx +++ b/superset-frontend/src/SqlLab/components/QueryHistory/index.tsx @@ -29,6 +29,8 @@ import { SqlLabRootState } from 'src/SqlLab/types'; import { useEditorQueriesQuery } from 'src/hooks/apiResources/queries'; import useEffectEvent from 'src/hooks/useEffectEvent'; import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor'; +import PanelToolbar from 'src/components/PanelToolbar'; +import { ViewContribution } from 'src/SqlLab/contributions'; interface QueryHistoryProps { queryEditorId: string | number; @@ -119,6 +121,7 @@ const QueryHistory = ({ return editorQueries.length > 0 ? ( <> + 2 * theme.sizeUnit}px; `; -const CopyStyledButton = styled(Button)` - &:hover { - color: ${({ theme }) => theme.colorPrimary}; - text-decoration: unset; - } - - span > :first-of-type { - margin: 0; - } -`; - -const ROWS_CHIP_WIDTH = 100; const GAP = 8; const extensionsRegistry = getExtensionsRegistry(); @@ -389,8 +373,71 @@ const ResultSet = ({ } }; + const defaultPrimaryActions = ( + <> + {visualize && database?.allows_virtual_table_explore && ( + + )} + {csv && canExportData && ( + - , - ); - - expect( - screen.getByRole('button', { name: 'Child Button' }), - ).toBeInTheDocument(); -}); - -test('renders primary actions from extension contributions', async () => { - const manager = ExtensionsManager.getInstance(); - - await createActivatedExtension(manager, { - commands: [createMockCommand('test.action')], - menus: { - [TEST_VIEW_ID]: createMockMenu({ - primary: [createMockMenuItem('test-view', 'test.action')], - }), - }, - }); - - render(); - - expect(screen.getByText('test.action Title')).toBeInTheDocument(); -}); - -test('renders primary actions with children', async () => { - const manager = ExtensionsManager.getInstance(); - - await createActivatedExtension(manager, { - commands: [createMockCommand('test.action')], - menus: { - [TEST_VIEW_ID]: createMockMenu({ - primary: [createMockMenuItem('test-view', 'test.action')], - }), - }, - }); - - render( - - - , - ); - - expect(screen.getByText('test.action Title')).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: 'Child Button' }), - ).toBeInTheDocument(); -}); - -test('hides title in compact mode for primary actions', async () => { - const manager = ExtensionsManager.getInstance(); - - await createActivatedExtension(manager, { - commands: [createMockCommand('test.action')], - menus: { - [TEST_VIEW_ID]: createMockMenu({ - primary: [createMockMenuItem('test-view', 'test.action')], - }), - }, - }); - - render(); - - expect(screen.queryByText('test.action Title')).not.toBeInTheDocument(); - expect(screen.getByRole('button')).toBeInTheDocument(); -}); - -test('executes command when primary action button is clicked', async () => { - const manager = ExtensionsManager.getInstance(); - - await createActivatedExtension(manager, { - commands: [createMockCommand('test.action')], - menus: { - [TEST_VIEW_ID]: createMockMenu({ - primary: [createMockMenuItem('test-view', 'test.action')], - }), - }, - }); - - render(); - - const button = screen.getByRole('button', { name: 'test.action Title' }); - await userEvent.click(button); - - expect(commands.executeCommand).toHaveBeenCalledWith('test.action'); -}); - -test('returns null when secondary mode with no actions and no defaultItems', () => { - const { container } = render( - , - ); - - expect(container).toBeEmptyDOMElement(); -}); - -test('renders dropdown button when secondary mode with defaultItems', () => { - render( - , - ); - - expect(screen.getByRole('button')).toBeInTheDocument(); -}); - -test('renders dropdown menu with defaultItems when clicked', async () => { - render( - , - ); - - const dropdownButton = screen.getByRole('button'); - await userEvent.click(dropdownButton); - - await waitFor(() => { - expect(screen.getByText('Item 1')).toBeInTheDocument(); - expect(screen.getByText('Item 2')).toBeInTheDocument(); - }); -}); - -test('renders secondary actions from extension contributions', async () => { - const manager = ExtensionsManager.getInstance(); - - await createActivatedExtension(manager, { - commands: [createMockCommand('test.secondary')], - menus: { - [TEST_VIEW_ID]: createMockMenu({ - secondary: [createMockMenuItem('test-view', 'test.secondary')], - }), - }, - }); - - render(); - - const dropdownButton = screen.getByRole('button'); - await userEvent.click(dropdownButton); - - await waitFor(() => { - expect(screen.getByText('test.secondary Title')).toBeInTheDocument(); - }); -}); - -test('merges extension secondary actions with defaultItems', async () => { - const manager = ExtensionsManager.getInstance(); - - await createActivatedExtension(manager, { - commands: [createMockCommand('test.secondary')], - menus: { - [TEST_VIEW_ID]: createMockMenu({ - secondary: [createMockMenuItem('test-view', 'test.secondary')], - }), - }, - }); - - render( - , - ); - - const dropdownButton = screen.getByRole('button'); - await userEvent.click(dropdownButton); - - await waitFor(() => { - expect(screen.getByText('test.secondary Title')).toBeInTheDocument(); - expect(screen.getByText('Default Item')).toBeInTheDocument(); - }); -}); - -test('executes command when secondary menu item is clicked', async () => { - const manager = ExtensionsManager.getInstance(); - - await createActivatedExtension(manager, { - commands: [createMockCommand('test.secondary')], - menus: { - [TEST_VIEW_ID]: createMockMenu({ - secondary: [createMockMenuItem('test-view', 'test.secondary')], - }), - }, - }); - - render(); - - const dropdownButton = screen.getByRole('button'); - await userEvent.click(dropdownButton); - - await waitFor(() => { - expect(screen.getByText('test.secondary Title')).toBeInTheDocument(); - }); - - const menuItem = screen.getByText('test.secondary Title'); - await userEvent.click(menuItem); - - expect(commands.executeCommand).toHaveBeenCalledWith('test.secondary'); -}); - -test('renders multiple primary actions from multiple contributions', async () => { - const manager = ExtensionsManager.getInstance(); - - await createActivatedExtension(manager, { - commands: [ - createMockCommand('test.action1'), - createMockCommand('test.action2'), - ], - menus: { - [TEST_VIEW_ID]: createMockMenu({ - primary: [ - createMockMenuItem('test-view1', 'test.action1'), - createMockMenuItem('test-view2', 'test.action2'), - ], - }), - }, - }); - - render(); - - expect(await screen.findByText('test.action1 Title')).toBeInTheDocument(); - expect(screen.getByText('test.action2 Title')).toBeInTheDocument(); -}); - -test('handles viewId with no matching contributions', () => { - render( - - - , - ); - - expect(screen.getByRole('button', { name: 'Fallback' })).toBeInTheDocument(); -}); diff --git a/superset-frontend/src/components/MenuListExtension/index.tsx b/superset-frontend/src/components/MenuListExtension/index.tsx deleted file mode 100644 index 98608f6a291..00000000000 --- a/superset-frontend/src/components/MenuListExtension/index.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/** - * 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 { css, useTheme } from '@apache-superset/core/ui'; -import { Button, Dropdown } from '@superset-ui/core/components'; -import { Menu, MenuItemType } from '@superset-ui/core/components/Menu'; -import { Icons } from '@superset-ui/core/components/Icons'; -import { commands } from 'src/core'; -import ExtensionsManager from 'src/extensions/ExtensionsManager'; - -export type MenuListExtensionProps = { - viewId: string; -} & ( - | { - primary: boolean; - secondary?: never; - children?: React.ReactNode; - defaultItems?: never; - compactMode?: boolean; - } - | { - primary?: never; - secondary: boolean; - children?: never; - defaultItems?: MenuItemType[]; - compactMode?: never; - } -); - -const MenuListExtension = ({ - viewId, - primary, - secondary, - defaultItems, - children, - compactMode, -}: MenuListExtensionProps) => { - const theme = useTheme(); - const contributions = - ExtensionsManager.getInstance().getMenuContributions(viewId); - - const actions = primary ? contributions?.primary : contributions?.secondary; - const primaryActions = useMemo( - () => - primary - ? (actions || []).map(contribution => { - const command = - ExtensionsManager.getInstance().getCommandContribution( - contribution.command, - )!; - if (!command?.icon) { - return null; - } - const Icon = - (Icons as Record)[ - command.icon - ] ?? Icons.FileOutlined; - - return ( - - ); - }) - : [], - [actions, primary, compactMode], - ); - const secondaryActions = useMemo( - () => - secondary - ? (actions || []) - .map(contribution => { - const command = - ExtensionsManager.getInstance().getCommandContribution( - contribution.command, - )!; - if (!command) { - return null; - } - return { - key: command.command, - label: command.title, - title: command.description, - onClick: () => commands.executeCommand(command.command), - } as MenuItemType; - }) - .concat(...(defaultItems || [])) - .filter(Boolean) - : [], - [actions, secondary, defaultItems], - ); - - if (secondary && secondaryActions.length === 0) { - return null; - } - - if (secondary) { - return ( - ( - div { - gap: ${theme.sizeUnit * 4}px; - } - `} - items={secondaryActions} - /> - )} - trigger={['click']} - > - - - ); - } - return ( - <> - {primaryActions} - {children} - - ); -}; - -export default MenuListExtension; diff --git a/superset-frontend/src/components/PanelToolbar/index.tsx b/superset-frontend/src/components/PanelToolbar/index.tsx new file mode 100644 index 00000000000..7f88197811b --- /dev/null +++ b/superset-frontend/src/components/PanelToolbar/index.tsx @@ -0,0 +1,165 @@ +/** + * 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 { css, useTheme } from '@apache-superset/core/ui'; +import { Button, Divider, Dropdown } from '@superset-ui/core/components'; +import { Menu, MenuItemType } from '@superset-ui/core/components/Menu'; +import { Icons } from '@superset-ui/core/components/Icons'; +import { commands } from 'src/core'; +import ExtensionsManager from 'src/extensions/ExtensionsManager'; + +export interface PanelToolbarProps { + viewId: string; + defaultPrimaryActions?: React.ReactNode; + defaultSecondaryActions?: MenuItemType[]; +} + +const PanelToolbar = ({ + viewId, + defaultPrimaryActions, + defaultSecondaryActions, +}: PanelToolbarProps) => { + const theme = useTheme(); + const contributions = + ExtensionsManager.getInstance().getMenuContributions(viewId); + + const primaryContributions = contributions?.primary || []; + const secondaryContributions = contributions?.secondary || []; + + const extensionPrimaryActions = useMemo( + () => + primaryContributions + .map(contribution => { + const command = + ExtensionsManager.getInstance().getCommandContribution( + contribution.command, + )!; + if (!command?.icon) { + return null; + } + const Icon = + (Icons as Record)[ + command.icon + ] ?? Icons.FileOutlined; + + return ( + + + )} + + ); +}; + +export default PanelToolbar;