feat: Create dataset header component (#21189)

This commit is contained in:
Lyndsi Kay Williams 2022-09-13 19:39:02 -05:00 committed by GitHub
parent 973d870538
commit 6e8cad3e16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 219 additions and 32 deletions

View File

@ -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

View File

@ -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 (

View File

@ -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

View File

@ -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');
}); });
}); });

View File

@ -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>
);
} }

View File

@ -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()}

View File

@ -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;
} }

View File

@ -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 () => {

View File

@ -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>

View File

@ -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};
}
`;