mirror of
https://github.com/apache/superset.git
synced 2024-09-06 13:57:40 -04:00
feat: Create dataset header component (#21189)
This commit is contained in:
parent
973d870538
commit
6e8cad3e16
@ -19,6 +19,7 @@
|
|||||||
import React, { ReactNode, ReactElement } from 'react';
|
import React, { ReactNode, ReactElement } from 'react';
|
||||||
import { css, SupersetTheme, t, useTheme } from '@superset-ui/core';
|
import { css, SupersetTheme, t, useTheme } from '@superset-ui/core';
|
||||||
import { AntdDropdown, AntdDropdownProps } from 'src/components';
|
import { AntdDropdown, AntdDropdownProps } from 'src/components';
|
||||||
|
import { TooltipPlacement } from 'src/components/Tooltip';
|
||||||
import {
|
import {
|
||||||
DynamicEditableTitle,
|
DynamicEditableTitle,
|
||||||
DynamicEditableTitleProps,
|
DynamicEditableTitleProps,
|
||||||
@ -112,6 +113,10 @@ export type PageHeaderWithActionsProps = {
|
|||||||
rightPanelAdditionalItems: ReactNode;
|
rightPanelAdditionalItems: ReactNode;
|
||||||
additionalActionsMenu: ReactElement;
|
additionalActionsMenu: ReactElement;
|
||||||
menuDropdownProps: Omit<AntdDropdownProps, 'overlay'>;
|
menuDropdownProps: Omit<AntdDropdownProps, 'overlay'>;
|
||||||
|
tooltipProps?: {
|
||||||
|
text?: string;
|
||||||
|
placement?: TooltipPlacement;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeaderWithActions = ({
|
export const PageHeaderWithActions = ({
|
||||||
@ -124,6 +129,7 @@ export const PageHeaderWithActions = ({
|
|||||||
rightPanelAdditionalItems,
|
rightPanelAdditionalItems,
|
||||||
additionalActionsMenu,
|
additionalActionsMenu,
|
||||||
menuDropdownProps,
|
menuDropdownProps,
|
||||||
|
tooltipProps,
|
||||||
}: PageHeaderWithActionsProps) => {
|
}: PageHeaderWithActionsProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
@ -152,6 +158,8 @@ export const PageHeaderWithActions = ({
|
|||||||
css={menuTriggerStyles}
|
css={menuTriggerStyles}
|
||||||
buttonStyle="tertiary"
|
buttonStyle="tertiary"
|
||||||
aria-label={t('Menu actions trigger')}
|
aria-label={t('Menu actions trigger')}
|
||||||
|
tooltip={tooltipProps?.text}
|
||||||
|
placement={tooltipProps?.placement}
|
||||||
data-test="actions-trigger"
|
data-test="actions-trigger"
|
||||||
>
|
>
|
||||||
<Icons.MoreHoriz
|
<Icons.MoreHoriz
|
||||||
|
@ -19,9 +19,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTheme, css } from '@superset-ui/core';
|
import { useTheme, css } from '@superset-ui/core';
|
||||||
import { Tooltip as AntdTooltip } from 'antd';
|
import { Tooltip as AntdTooltip } from 'antd';
|
||||||
import { TooltipProps } from 'antd/lib/tooltip';
|
import {
|
||||||
|
TooltipProps,
|
||||||
|
TooltipPlacement as AntdTooltipPlacement,
|
||||||
|
} from 'antd/lib/tooltip';
|
||||||
import { Global } from '@emotion/react';
|
import { Global } from '@emotion/react';
|
||||||
|
|
||||||
|
export type TooltipPlacement = AntdTooltipPlacement;
|
||||||
|
|
||||||
export const Tooltip = (props: TooltipProps) => {
|
export const Tooltip = (props: TooltipProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
|
@ -26,9 +26,12 @@ describe('AddDataset', () => {
|
|||||||
|
|
||||||
const blankeStateImgs = screen.getAllByRole('img', { name: /empty/i });
|
const blankeStateImgs = screen.getAllByRole('img', { name: /empty/i });
|
||||||
|
|
||||||
expect(await screen.findByText(/header/i)).toBeInTheDocument();
|
|
||||||
// Header
|
// Header
|
||||||
expect(screen.getByText(/header/i)).toBeVisible();
|
expect(
|
||||||
|
await screen.findByRole('textbox', {
|
||||||
|
name: /dataset name/i,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
// Left panel
|
// Left panel
|
||||||
expect(blankeStateImgs[0]).toBeVisible();
|
expect(blankeStateImgs[0]).toBeVisible();
|
||||||
// Footer
|
// Footer
|
||||||
|
@ -16,14 +16,64 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
import Header from 'src/views/CRUD/data/dataset/AddDataset/Header';
|
import Header from 'src/views/CRUD/data/dataset/AddDataset/Header';
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
it('renders a blank state Header', () => {
|
const mockSetDataset = jest.fn();
|
||||||
render(<Header />);
|
|
||||||
|
|
||||||
expect(screen.getByText(/header/i)).toBeVisible();
|
const waitForRender = (datasetName: string) =>
|
||||||
|
waitFor(() =>
|
||||||
|
render(<Header setDataset={mockSetDataset} datasetName={datasetName} />),
|
||||||
|
);
|
||||||
|
|
||||||
|
it('renders a blank state Header', async () => {
|
||||||
|
await waitForRender('');
|
||||||
|
|
||||||
|
const datasetNameTextbox = screen.getByRole('textbox', {
|
||||||
|
name: /dataset name/i,
|
||||||
|
});
|
||||||
|
const saveButton = screen.getByRole('button', {
|
||||||
|
name: /save save/i,
|
||||||
|
});
|
||||||
|
const menuButton = screen.getByRole('button', {
|
||||||
|
name: /menu actions trigger/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(datasetNameTextbox).toBeVisible();
|
||||||
|
expect(saveButton).toBeVisible();
|
||||||
|
expect(saveButton).toBeDisabled();
|
||||||
|
expect(menuButton).toBeVisible();
|
||||||
|
expect(menuButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates display value of dataset name textbox when Header title is changed', async () => {
|
||||||
|
await waitForRender('');
|
||||||
|
|
||||||
|
const datasetNameTextbox = screen.getByRole('textbox', {
|
||||||
|
name: /dataset name/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Textbox should start with an empty display value and placeholder text
|
||||||
|
expect(datasetNameTextbox).toHaveDisplayValue('');
|
||||||
|
expect(
|
||||||
|
screen.getByPlaceholderText(/add the name of the dataset/i),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Textbox should update its display value when user inputs a new value
|
||||||
|
userEvent.type(datasetNameTextbox, 'Test name');
|
||||||
|
expect(datasetNameTextbox).toHaveDisplayValue('Test name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes an existing dataset title into the dataset name textbox', async () => {
|
||||||
|
await waitForRender('Existing Dataset Name');
|
||||||
|
|
||||||
|
const datasetNameTextbox = screen.getByRole('textbox', {
|
||||||
|
name: /dataset name/i,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(datasetNameTextbox).toHaveDisplayValue('Existing Dataset Name');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,7 +17,81 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { t } from '@superset-ui/core';
|
||||||
|
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
|
||||||
|
import Button from 'src/components/Button';
|
||||||
|
import Icons from 'src/components/Icons';
|
||||||
|
import { Menu } from 'src/components/Menu';
|
||||||
|
import { TooltipPlacement } from 'src/components/Tooltip';
|
||||||
|
import {
|
||||||
|
HeaderComponentStyles,
|
||||||
|
disabledSaveBtnStyles,
|
||||||
|
} from 'src/views/CRUD/data/dataset/styles';
|
||||||
|
import {
|
||||||
|
DatasetActionType,
|
||||||
|
DSReducerActionType,
|
||||||
|
} from 'src/views/CRUD/data/dataset/AddDataset/types';
|
||||||
|
|
||||||
export default function Header() {
|
const tooltipProps: { text: string; placement: TooltipPlacement } = {
|
||||||
return <div>Header</div>;
|
text: t('Select a database table and create dataset'),
|
||||||
|
placement: 'bottomRight',
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDisabledSaveButton = () => (
|
||||||
|
<Button
|
||||||
|
buttonStyle="primary"
|
||||||
|
tooltip={tooltipProps?.text}
|
||||||
|
placement={tooltipProps?.placement}
|
||||||
|
disabled
|
||||||
|
css={disabledSaveBtnStyles}
|
||||||
|
>
|
||||||
|
<Icons.Save iconSize="m" />
|
||||||
|
{t('Save')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderOverlay = () => (
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>{t('Settings')}</Menu.Item>
|
||||||
|
<Menu.Item>{t('Delete')}</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function Header({
|
||||||
|
setDataset,
|
||||||
|
datasetName,
|
||||||
|
}: {
|
||||||
|
setDataset: React.Dispatch<DSReducerActionType>;
|
||||||
|
datasetName: string;
|
||||||
|
}) {
|
||||||
|
const editableTitleProps = {
|
||||||
|
title: datasetName,
|
||||||
|
placeholder: t('Add the name of the dataset'),
|
||||||
|
onSave: (newDatasetName: string) => {
|
||||||
|
setDataset({
|
||||||
|
type: DatasetActionType.changeDataset,
|
||||||
|
payload: { name: 'dataset_name', value: newDatasetName },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
canEdit: true,
|
||||||
|
label: t('dataset name'),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeaderComponentStyles>
|
||||||
|
<PageHeaderWithActions
|
||||||
|
editableTitleProps={editableTitleProps}
|
||||||
|
showTitlePanelItems={false}
|
||||||
|
showFaveStar={false}
|
||||||
|
faveStarProps={{ itemId: 1, saveFaveStar: () => {} }}
|
||||||
|
titlePanelAdditionalItems={<></>}
|
||||||
|
rightPanelAdditionalItems={renderDisabledSaveButton()}
|
||||||
|
additionalActionsMenu={renderOverlay()}
|
||||||
|
menuDropdownProps={{
|
||||||
|
disabled: true,
|
||||||
|
}}
|
||||||
|
tooltipProps={tooltipProps}
|
||||||
|
/>
|
||||||
|
</HeaderComponentStyles>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,10 @@ export default function AddDataset() {
|
|||||||
Reducer<Partial<DatasetObject> | null, DSReducerActionType>
|
Reducer<Partial<DatasetObject> | null, DSReducerActionType>
|
||||||
>(datasetReducer, null);
|
>(datasetReducer, null);
|
||||||
|
|
||||||
|
const HeaderComponent = () => (
|
||||||
|
<Header setDataset={setDataset} datasetName={dataset?.dataset_name ?? ''} />
|
||||||
|
);
|
||||||
|
|
||||||
const LeftPanelComponent = () => (
|
const LeftPanelComponent = () => (
|
||||||
<LeftPanel
|
<LeftPanel
|
||||||
setDataset={setDataset}
|
setDataset={setDataset}
|
||||||
@ -80,7 +84,7 @@ export default function AddDataset() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DatasetLayout
|
<DatasetLayout
|
||||||
header={Header()}
|
header={HeaderComponent()}
|
||||||
leftPanel={LeftPanelComponent()}
|
leftPanel={LeftPanelComponent()}
|
||||||
datasetPanel={DatasetPanel()}
|
datasetPanel={DatasetPanel()}
|
||||||
footer={Footer()}
|
footer={Footer()}
|
||||||
|
@ -32,7 +32,7 @@ export interface DatasetObject {
|
|||||||
table_name?: string | null;
|
table_name?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DatasetReducerPayloadType {
|
export interface DatasetReducerPayloadType {
|
||||||
name: string;
|
name: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from 'spec/helpers/testing-library';
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
import DatasetLayout from 'src/views/CRUD/data/dataset/DatasetLayout';
|
import DatasetLayout from 'src/views/CRUD/data/dataset/DatasetLayout';
|
||||||
import Header from 'src/views/CRUD/data/dataset/AddDataset/Header';
|
import Header from 'src/views/CRUD/data/dataset/AddDataset/Header';
|
||||||
import LeftPanel from 'src/views/CRUD/data/dataset/AddDataset/LeftPanel';
|
import LeftPanel from 'src/views/CRUD/data/dataset/AddDataset/LeftPanel';
|
||||||
@ -33,10 +33,21 @@ describe('DatasetLayout', () => {
|
|||||||
expect(layoutWrapper).toHaveTextContent('');
|
expect(layoutWrapper).toHaveTextContent('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a Header when passed in', () => {
|
const mockSetDataset = jest.fn();
|
||||||
render(<DatasetLayout header={Header()} />);
|
|
||||||
|
|
||||||
expect(screen.getByText(/header/i)).toBeVisible();
|
const waitForRender = () =>
|
||||||
|
waitFor(() =>
|
||||||
|
render(<Header setDataset={mockSetDataset} datasetName="" />),
|
||||||
|
);
|
||||||
|
|
||||||
|
it('renders a Header when passed in', async () => {
|
||||||
|
await waitForRender();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('textbox', {
|
||||||
|
name: /dataset name/i,
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a LeftPanel when passed in', async () => {
|
it('renders a LeftPanel when passed in', async () => {
|
||||||
|
@ -24,11 +24,11 @@ import {
|
|||||||
OuterRow,
|
OuterRow,
|
||||||
PanelRow,
|
PanelRow,
|
||||||
FooterRow,
|
FooterRow,
|
||||||
StyledHeader,
|
StyledLayoutHeader,
|
||||||
StyledLeftPanel,
|
StyledLayoutLeftPanel,
|
||||||
StyledDatasetPanel,
|
StyledLayoutDatasetPanel,
|
||||||
StyledRightPanel,
|
StyledLayoutRightPanel,
|
||||||
StyledFooter,
|
StyledLayoutFooter,
|
||||||
} from 'src/views/CRUD/data/dataset/styles';
|
} from 'src/views/CRUD/data/dataset/styles';
|
||||||
|
|
||||||
interface DatasetLayoutProps {
|
interface DatasetLayoutProps {
|
||||||
@ -48,22 +48,28 @@ export default function DatasetLayout({
|
|||||||
}: DatasetLayoutProps) {
|
}: DatasetLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<StyledLayoutWrapper data-test="dataset-layout-wrapper">
|
<StyledLayoutWrapper data-test="dataset-layout-wrapper">
|
||||||
{header && <StyledHeader>{header}</StyledHeader>}
|
{header && <StyledLayoutHeader>{header}</StyledLayoutHeader>}
|
||||||
<OuterRow>
|
<OuterRow>
|
||||||
<LeftColumn>
|
<LeftColumn>
|
||||||
{leftPanel && <StyledLeftPanel>{leftPanel}</StyledLeftPanel>}
|
{leftPanel && (
|
||||||
|
<StyledLayoutLeftPanel>{leftPanel}</StyledLayoutLeftPanel>
|
||||||
|
)}
|
||||||
</LeftColumn>
|
</LeftColumn>
|
||||||
|
|
||||||
<RightColumn>
|
<RightColumn>
|
||||||
<PanelRow>
|
<PanelRow>
|
||||||
{datasetPanel && (
|
{datasetPanel && (
|
||||||
<StyledDatasetPanel>{datasetPanel}</StyledDatasetPanel>
|
<StyledLayoutDatasetPanel>
|
||||||
|
{datasetPanel}
|
||||||
|
</StyledLayoutDatasetPanel>
|
||||||
|
)}
|
||||||
|
{rightPanel && (
|
||||||
|
<StyledLayoutRightPanel>{rightPanel}</StyledLayoutRightPanel>
|
||||||
)}
|
)}
|
||||||
{rightPanel && <StyledRightPanel>{rightPanel}</StyledRightPanel>}
|
|
||||||
</PanelRow>
|
</PanelRow>
|
||||||
|
|
||||||
<FooterRow>
|
<FooterRow>
|
||||||
{footer && <StyledFooter>{footer}</StyledFooter>}
|
{footer && <StyledLayoutFooter>{footer}</StyledLayoutFooter>}
|
||||||
</FooterRow>
|
</FooterRow>
|
||||||
</RightColumn>
|
</RightColumn>
|
||||||
</OuterRow>
|
</OuterRow>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { styled } from '@superset-ui/core';
|
import { styled, css, SupersetTheme } from '@superset-ui/core';
|
||||||
|
|
||||||
export const StyledLayoutWrapper = styled.div`
|
export const StyledLayoutWrapper = styled.div`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@ -64,32 +64,58 @@ export const FooterRow = styled(Row)`
|
|||||||
height: ${({ theme }) => theme.gridUnit * 16}px;
|
height: ${({ theme }) => theme.gridUnit * 16}px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledHeader = styled.div`
|
export const StyledLayoutHeader = styled.div`
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
height: ${({ theme }) => theme.gridUnit * 16}px;
|
height: ${({ theme }) => theme.gridUnit * 16}px;
|
||||||
border-bottom: 2px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-bottom: 2px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
color: ${({ theme }) => theme.colors.error.base};
|
|
||||||
|
.header-with-actions {
|
||||||
|
height: ${({ theme }) => theme.gridUnit * 15.5}px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledLeftPanel = styled.div`
|
export const StyledLayoutLeftPanel = styled.div`
|
||||||
width: ${({ theme }) => theme.gridUnit * 80}px;
|
width: ${({ theme }) => theme.gridUnit * 80}px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledDatasetPanel = styled.div`
|
export const StyledLayoutDatasetPanel = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledRightPanel = styled.div`
|
export const StyledLayoutRightPanel = styled.div`
|
||||||
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
color: ${({ theme }) => theme.colors.success.base};
|
color: ${({ theme }) => theme.colors.success.base};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledFooter = styled.div`
|
export const StyledLayoutFooter = styled.div`
|
||||||
height: ${({ theme }) => theme.gridUnit * 16}px;
|
height: ${({ theme }) => theme.gridUnit * 16}px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
|
||||||
color: ${({ theme }) => theme.colors.info.base};
|
color: ${({ theme }) => theme.colors.info.base};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const HeaderComponentStyles = styled.div`
|
||||||
|
.ant-btn {
|
||||||
|
span {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
svg {
|
||||||
|
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const disabledSaveBtnStyles = (theme: SupersetTheme) => css`
|
||||||
|
width: ${theme.gridUnit * 21.5}px;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background-color: ${theme.colors.grayscale.light3};
|
||||||
|
color: ${theme.colors.grayscale.light1};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
Loading…
Reference in New Issue
Block a user