diff --git a/superset-frontend/src/components/PageHeaderWithActions/index.tsx b/superset-frontend/src/components/PageHeaderWithActions/index.tsx index 19f1ded4a7..9209ab818d 100644 --- a/superset-frontend/src/components/PageHeaderWithActions/index.tsx +++ b/superset-frontend/src/components/PageHeaderWithActions/index.tsx @@ -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; + 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" > { const theme = useTheme(); return ( diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/AddDataset.test.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/AddDataset.test.tsx index 6a5bf1ea35..53ece88824 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/AddDataset.test.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/AddDataset.test.tsx @@ -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 diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Header/Header.test.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Header/Header.test.tsx index c913dbc86a..d4058d8ca7 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Header/Header.test.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Header/Header.test.tsx @@ -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(
); + const mockSetDataset = jest.fn(); - expect(screen.getByText(/header/i)).toBeVisible(); + const waitForRender = (datasetName: string) => + waitFor(() => + render(
), + ); + + 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'); }); }); diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Header/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Header/index.tsx index 44f0e19f7b..b4cf81d032 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Header/index.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Header/index.tsx @@ -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
Header
; +const tooltipProps: { text: string; placement: TooltipPlacement } = { + text: t('Select a database table and create dataset'), + placement: 'bottomRight', +}; + +const renderDisabledSaveButton = () => ( + +); + +const renderOverlay = () => ( + + {t('Settings')} + {t('Delete')} + +); + +export default function Header({ + setDataset, + datasetName, +}: { + setDataset: React.Dispatch; + 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 ( + + {} }} + titlePanelAdditionalItems={<>} + rightPanelAdditionalItems={renderDisabledSaveButton()} + additionalActionsMenu={renderOverlay()} + menuDropdownProps={{ + disabled: true, + }} + tooltipProps={tooltipProps} + /> + + ); } diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/index.tsx index a1ec33ad17..79c6e5f4c3 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/index.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/index.tsx @@ -70,6 +70,10 @@ export default function AddDataset() { Reducer | null, DSReducerActionType> >(datasetReducer, null); + const HeaderComponent = () => ( +
+ ); + const LeftPanelComponent = () => ( { expect(layoutWrapper).toHaveTextContent(''); }); - it('renders a Header when passed in', () => { - render(); + const mockSetDataset = jest.fn(); - expect(screen.getByText(/header/i)).toBeVisible(); + const waitForRender = () => + waitFor(() => + render(
), + ); + + 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 () => { diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetLayout/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetLayout/index.tsx index 8587b25a4a..b5691fe5cb 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/DatasetLayout/index.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetLayout/index.tsx @@ -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 ( - {header && {header}} + {header && {header}} - {leftPanel && {leftPanel}} + {leftPanel && ( + {leftPanel} + )} {datasetPanel && ( - {datasetPanel} + + {datasetPanel} + + )} + {rightPanel && ( + {rightPanel} )} - {rightPanel && {rightPanel}} - {footer && {footer}} + {footer && {footer}} diff --git a/superset-frontend/src/views/CRUD/data/dataset/styles.ts b/superset-frontend/src/views/CRUD/data/dataset/styles.ts index 8d9f771dce..757837f221 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/styles.ts +++ b/superset-frontend/src/views/CRUD/data/dataset/styles.ts @@ -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}; + } +`;