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 { css, SupersetTheme, t, useTheme } from '@superset-ui/core';
import { AntdDropdown, AntdDropdownProps } from 'src/components';
import { TooltipPlacement } from 'src/components/Tooltip';
import {
DynamicEditableTitle,
DynamicEditableTitleProps,
@ -112,6 +113,10 @@ export type PageHeaderWithActionsProps = {
rightPanelAdditionalItems: ReactNode;
additionalActionsMenu: ReactElement;
menuDropdownProps: Omit<AntdDropdownProps, 'overlay'>;
tooltipProps?: {
text?: string;
placement?: TooltipPlacement;
};
};
export const PageHeaderWithActions = ({
@ -124,6 +129,7 @@ export const PageHeaderWithActions = ({
rightPanelAdditionalItems,
additionalActionsMenu,
menuDropdownProps,
tooltipProps,
}: PageHeaderWithActionsProps) => {
const theme = useTheme();
return (
@ -152,6 +158,8 @@ export const PageHeaderWithActions = ({
css={menuTriggerStyles}
buttonStyle="tertiary"
aria-label={t('Menu actions trigger')}
tooltip={tooltipProps?.text}
placement={tooltipProps?.placement}
data-test="actions-trigger"
>
<Icons.MoreHoriz

View File

@ -19,9 +19,14 @@
import React from 'react';
import { useTheme, css } from '@superset-ui/core';
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';
export type TooltipPlacement = AntdTooltipPlacement;
export const Tooltip = (props: TooltipProps) => {
const theme = useTheme();
return (

View File

@ -26,9 +26,12 @@ describe('AddDataset', () => {
const blankeStateImgs = screen.getAllByRole('img', { name: /empty/i });
expect(await screen.findByText(/header/i)).toBeInTheDocument();
// Header
expect(screen.getByText(/header/i)).toBeVisible();
expect(
await screen.findByRole('textbox', {
name: /dataset name/i,
}),
).toBeVisible();
// Left panel
expect(blankeStateImgs[0]).toBeVisible();
// Footer

View File

@ -16,14 +16,64 @@
* specific language governing permissions and limitations
* under the License.
*/
import userEvent from '@testing-library/user-event';
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';
describe('Header', () => {
it('renders a blank state Header', () => {
render(<Header />);
const mockSetDataset = jest.fn();
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.
*/
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() {
return <div>Header</div>;
const tooltipProps: { text: string; placement: TooltipPlacement } = {
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>
>(datasetReducer, null);
const HeaderComponent = () => (
<Header setDataset={setDataset} datasetName={dataset?.dataset_name ?? ''} />
);
const LeftPanelComponent = () => (
<LeftPanel
setDataset={setDataset}
@ -80,7 +84,7 @@ export default function AddDataset() {
return (
<DatasetLayout
header={Header()}
header={HeaderComponent()}
leftPanel={LeftPanelComponent()}
datasetPanel={DatasetPanel()}
footer={Footer()}

View File

@ -32,7 +32,7 @@ export interface DatasetObject {
table_name?: string | null;
}
interface DatasetReducerPayloadType {
export interface DatasetReducerPayloadType {
name: string;
value?: string;
}

View File

@ -17,7 +17,7 @@
* under the License.
*/
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 Header from 'src/views/CRUD/data/dataset/AddDataset/Header';
import LeftPanel from 'src/views/CRUD/data/dataset/AddDataset/LeftPanel';
@ -33,10 +33,21 @@ describe('DatasetLayout', () => {
expect(layoutWrapper).toHaveTextContent('');
});
it('renders a Header when passed in', () => {
render(<DatasetLayout header={Header()} />);
const mockSetDataset = jest.fn();
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 () => {

View File

@ -24,11 +24,11 @@ import {
OuterRow,
PanelRow,
FooterRow,
StyledHeader,
StyledLeftPanel,
StyledDatasetPanel,
StyledRightPanel,
StyledFooter,
StyledLayoutHeader,
StyledLayoutLeftPanel,
StyledLayoutDatasetPanel,
StyledLayoutRightPanel,
StyledLayoutFooter,
} from 'src/views/CRUD/data/dataset/styles';
interface DatasetLayoutProps {
@ -48,22 +48,28 @@ export default function DatasetLayout({
}: DatasetLayoutProps) {
return (
<StyledLayoutWrapper data-test="dataset-layout-wrapper">
{header && <StyledHeader>{header}</StyledHeader>}
{header && <StyledLayoutHeader>{header}</StyledLayoutHeader>}
<OuterRow>
<LeftColumn>
{leftPanel && <StyledLeftPanel>{leftPanel}</StyledLeftPanel>}
{leftPanel && (
<StyledLayoutLeftPanel>{leftPanel}</StyledLayoutLeftPanel>
)}
</LeftColumn>
<RightColumn>
<PanelRow>
{datasetPanel && (
<StyledDatasetPanel>{datasetPanel}</StyledDatasetPanel>
<StyledLayoutDatasetPanel>
{datasetPanel}
</StyledLayoutDatasetPanel>
)}
{rightPanel && (
<StyledLayoutRightPanel>{rightPanel}</StyledLayoutRightPanel>
)}
{rightPanel && <StyledRightPanel>{rightPanel}</StyledRightPanel>}
</PanelRow>
<FooterRow>
{footer && <StyledFooter>{footer}</StyledFooter>}
{footer && <StyledLayoutFooter>{footer}</StyledLayoutFooter>}
</FooterRow>
</RightColumn>
</OuterRow>

View File

@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { styled } from '@superset-ui/core';
import { styled, css, SupersetTheme } from '@superset-ui/core';
export const StyledLayoutWrapper = styled.div`
flex-grow: 1;
@ -64,32 +64,58 @@ export const FooterRow = styled(Row)`
height: ${({ theme }) => theme.gridUnit * 16}px;
`;
export const StyledHeader = styled.div`
export const StyledLayoutHeader = styled.div`
flex: 0 0 auto;
height: ${({ theme }) => theme.gridUnit * 16}px;
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;
height: 100%;
border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
`;
export const StyledDatasetPanel = styled.div`
export const StyledLayoutDatasetPanel = styled.div`
width: 100%;
`;
export const StyledRightPanel = styled.div`
export const StyledLayoutRightPanel = styled.div`
border-left: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
color: ${({ theme }) => theme.colors.success.base};
`;
export const StyledFooter = styled.div`
export const StyledLayoutFooter = styled.div`
height: ${({ theme }) => theme.gridUnit * 16}px;
width: 100%;
border-top: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2};
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};
}
`;