Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* 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 { render, screen, userEvent } from '@superset-ui/core/spec';
import { Icons } from '@superset-ui/core/components/Icons';
import { ActionButton } from '.';

const defaultProps = {
label: 'test-action',
icon: <Icons.EditOutlined />,
onClick: jest.fn(),
};

test('renders action button with icon', () => {
render(<ActionButton {...defaultProps} />);

const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
expect(button).toHaveAttribute('data-test', 'test-action');
expect(button).toHaveClass('action-button');
});

test('calls onClick when clicked', async () => {
const onClick = jest.fn();
render(<ActionButton {...defaultProps} onClick={onClick} />);

const button = screen.getByRole('button');
userEvent.click(button);

expect(onClick).toHaveBeenCalledTimes(1);
});

test('renders with tooltip when tooltip prop is provided', async () => {
const tooltipText = 'This is a tooltip';
render(<ActionButton {...defaultProps} tooltip={tooltipText} />);

const button = screen.getByRole('button');
userEvent.hover(button);

const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(tooltip).toHaveTextContent(tooltipText);
});

test('renders without tooltip when tooltip prop is not provided', async () => {
render(<ActionButton {...defaultProps} />);

const button = screen.getByRole('button');
userEvent.hover(button);

const tooltip = screen.queryByRole('tooltip');
expect(tooltip).not.toBeInTheDocument();
});

test('supports ReactElement tooltip', async () => {
const tooltipElement = <div>Custom tooltip content</div>;
render(<ActionButton {...defaultProps} tooltip={tooltipElement} />);

const button = screen.getByRole('button');
userEvent.hover(button);

const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(tooltip).toHaveTextContent('Custom tooltip content');
});

test('renders different icons correctly', () => {
render(<ActionButton {...defaultProps} icon={<Icons.DeleteOutlined />} />);

const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
});

test('renders with custom placement for tooltip', async () => {
const tooltipText = 'Tooltip with custom placement';
render(
<ActionButton {...defaultProps} tooltip={tooltipText} placement="bottom" />,
);

const button = screen.getByRole('button');
userEvent.hover(button);

const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
});

test('has proper accessibility attributes', () => {
render(<ActionButton {...defaultProps} />);

const button = screen.getByRole('button');
expect(button).toHaveAttribute('tabIndex', '0');
expect(button).toHaveAttribute('role', 'button');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* 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 type { ReactElement } from 'react';
import {
Tooltip,
type TooltipPlacement,
type IconType,
} from '@superset-ui/core/components';
import { css, useTheme } from '@superset-ui/core';

export interface ActionProps {
label: string;
tooltip?: string | ReactElement;
placement?: TooltipPlacement;
icon: IconType;
onClick: () => void;
}

export const ActionButton = ({
label,
tooltip,
placement,
icon,
onClick,
}: ActionProps) => {
const theme = useTheme();
const actionButton = (
<span
role="button"
tabIndex={0}
css={css`
cursor: pointer;
color: ${theme.colorIcon};
margin-right: ${theme.sizeUnit}px;
&:hover {
path {
fill: ${theme.colorPrimary};
}
}
`}
className="action-button"
data-test={label}
onClick={onClick}
>
{icon}
</span>
);

const tooltipId = `${label.replaceAll(' ', '-').toLowerCase()}-tooltip`;

return tooltip ? (
<Tooltip id={tooltipId} title={tooltip} placement={placement}>
{actionButton}
</Tooltip>
) : (
actionButton
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@ import { css, styled, useTheme } from '@superset-ui/core';
// eslint-disable-next-line no-restricted-imports
import { Tabs as AntdTabs, TabsProps as AntdTabsProps } from 'antd';
import { Icons } from '@superset-ui/core/components/Icons';
import type { SerializedStyles } from '@emotion/react';

export interface TabsProps extends AntdTabsProps {
allowOverflow?: boolean;
contentStyle?: SerializedStyles;
}

const StyledTabs = ({
animated = false,
allowOverflow = true,
tabBarStyle,
contentStyle,
...props
}: TabsProps) => {
const theme = useTheme();
Expand All @@ -46,6 +49,7 @@ const StyledTabs = ({

.ant-tabs-content-holder {
overflow: ${allowOverflow ? 'visible' : 'auto'};
${contentStyle}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing null check for contentStyle CSS interpolation category Functionality

Tell me more
What is the issue?

The contentStyle prop is being interpolated directly into CSS without null/undefined checks, which could cause rendering issues if the prop is not provided.

Why this matters

When contentStyle is undefined or null, the CSS interpolation will render 'undefined' or 'null' as literal text in the CSS, potentially breaking styles or causing console warnings.

Suggested change ∙ Feature Preview

Add a null check before interpolating contentStyle:

${contentStyle || ''}

Or use optional chaining with nullish coalescing:

${contentStyle ?? ''}
Provide feedback to improve future suggestions

Nice Catch Incorrect Not in Scope Not in coding standard Other

💬 Looking for more details? Reply to this comment to chat with Korbit.

}
.ant-tabs-tab {
flex: 1 1 auto;
Expand Down Expand Up @@ -85,9 +89,10 @@ const Tabs = Object.assign(StyledTabs, {
});

const StyledEditableTabs = styled(StyledTabs)`
${({ theme }) => `
${({ theme, contentStyle }) => `
.ant-tabs-content-holder {
background: ${theme.colorBgContainer};
${contentStyle}
}

& > .ant-tabs-nav {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,4 @@ export {
type CodeEditorMode,
type CodeEditorTheme,
} from './CodeEditor';
export { ActionButton, type ActionProps } from './ActionButton';
10 changes: 8 additions & 2 deletions superset-frontend/src/SqlLab/components/QueryHistory/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
css,
FeatureFlag,
isFeatureEnabled,
useTheme,
} from '@superset-ui/core';
import QueryTable from 'src/SqlLab/components/QueryTable';
import { SqlLabRootState } from 'src/SqlLab/types';
Expand Down Expand Up @@ -67,6 +68,7 @@ const QueryHistory = ({
const { id, tabViewId } = useQueryEditor(String(queryEditorId), [
'tabViewId',
]);
const theme = useTheme();
const editorId = tabViewId ?? id;
const [ref, hasReachedBottom] = useInView({ threshold: 0 });
const [pageIndex, setPageIndex] = useState(0);
Expand Down Expand Up @@ -118,7 +120,11 @@ const QueryHistory = ({
}

return editorQueries.length > 0 ? (
<>
<div
css={css`
padding-left: ${theme.sizeUnit * 4}px;
`}
>
<QueryTable
columns={[
'state',
Expand All @@ -144,7 +150,7 @@ const QueryHistory = ({
/>
)}
{isFetching && <Skeleton active />}
</>
</div>
) : (
<StyledEmptyStateWrapper>
<EmptyState
Expand Down
3 changes: 3 additions & 0 deletions superset-frontend/src/SqlLab/components/ResultSet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ const ReturnedRows = styled.div`
const ResultSetControls = styled.div`
display: flex;
justify-content: space-between;
padding-left: ${({ theme }) => theme.sizeUnit * 4}px;
`;

const ResultSetButtons = styled.div`
Expand Down Expand Up @@ -669,6 +670,7 @@ const ResultSet = ({
css={css`
display: flex;
justify-content: space-between;
padding-left: ${theme.sizeUnit * 4}px;
align-items: center;
gap: ${GAP}px;
`}
Expand Down Expand Up @@ -704,6 +706,7 @@ const ResultSet = ({
<div
css={css`
flex: 1 1 auto;
padding-left: ${theme.sizeUnit * 4}px;
`}
>
<AutoSizer disableWidth>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ const StyledSqlEditor = styled.div`

.queryPane {
padding: ${theme.sizeUnit * 2}px;
padding-left: 0px;
+ .ant-splitter-bar .ant-splitter-bar-dragger {
&::before {
background: transparent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,32 +138,30 @@ test('renders preview', async () => {
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
describe('table actions', () => {
test('refreshes table metadata when triggered', async () => {
const { getByRole, getByText } = render(<TablePreview {...mockedProps} />, {
const { getByRole } = render(<TablePreview {...mockedProps} />, {
useRedux: true,
initialState,
});
await waitFor(() =>
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1),
);
const menuButton = getByRole('button', { name: /Table actions/i });
fireEvent.click(menuButton);
fireEvent.click(getByText('Refresh table schema'));
const refreshButton = getByRole('button', { name: 'sync' });
fireEvent.click(refreshButton);
await waitFor(() =>
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(2),
);
});

test('shows CREATE VIEW statement', async () => {
const { getByRole, getByText } = render(<TablePreview {...mockedProps} />, {
const { getByRole } = render(<TablePreview {...mockedProps} />, {
useRedux: true,
initialState,
});
await waitFor(() =>
expect(fetchMock.calls(getTableMetadataEndpoint)).toHaveLength(1),
);
const menuButton = getByRole('button', { name: /Table actions/i });
fireEvent.click(menuButton);
fireEvent.click(getByText('Show CREATE VIEW statement'));
const viewButton = getByRole('button', { name: 'eye' });
fireEvent.click(viewButton);
await waitFor(() =>
expect(
screen.queryByRole('dialog', { name: 'CREATE VIEW statement' }),
Expand Down
Loading
Loading