diff --git a/superset-frontend/.storybook/main.js b/superset-frontend/.storybook/main.js index b8f15b569f3d..814e53cf5841 100644 --- a/superset-frontend/.storybook/main.js +++ b/superset-frontend/.storybook/main.js @@ -24,8 +24,8 @@ module.exports = { builder: 'webpack5', }, stories: [ - '../src/@(components|common|filters|explore)/**/*.stories.@(tsx|jsx)', - '../src/@(components|common|filters|explore)/**/*.*.@(mdx)', + '../src/@(components|common|filters|explore|views)/**/*.stories.@(tsx|jsx)', + '../src/@(components|common|filters|explore|views)/**/*.*.@(mdx)', ], addons: [ '@storybook/addon-essentials', diff --git a/superset-frontend/src/assets/images/no-columns.svg b/superset-frontend/src/assets/images/no-columns.svg new file mode 100644 index 000000000000..2fc8fe0661bf --- /dev/null +++ b/superset-frontend/src/assets/images/no-columns.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/superset-frontend/src/components/EmptyState/index.tsx b/superset-frontend/src/components/EmptyState/index.tsx index 3f5d586cf8d9..982cace6672f 100644 --- a/superset-frontend/src/components/EmptyState/index.tsx +++ b/superset-frontend/src/components/EmptyState/index.tsx @@ -31,7 +31,7 @@ export enum EmptyStateSize { export interface EmptyStateSmallProps { title: ReactNode; description?: ReactNode; - image: ReactNode; + image?: ReactNode; } export interface EmptyStateProps extends EmptyStateSmallProps { @@ -156,7 +156,7 @@ export const EmptyStateBig = ({ className, }: EmptyStateProps) => ( - + {image && } css` @@ -187,7 +187,7 @@ export const EmptyStateMedium = ({ buttonText, }: EmptyStateProps) => ( - + {image && } css` @@ -216,7 +216,7 @@ export const EmptyStateSmall = ({ description, }: EmptyStateSmallProps) => ( - + {image && } css` diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.stories.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.stories.tsx new file mode 100644 index 000000000000..8a7fd7d6438f --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.stories.tsx @@ -0,0 +1,44 @@ +/** + * 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 React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { supersetTheme, ThemeProvider } from '@superset-ui/core'; +import DatasetPanel from './DatasetPanel'; +import { exampleColumns } from './fixtures'; + +export default { + title: 'Superset App/views/CRUD/data/dataset/DatasetPanel', + component: DatasetPanel, +} as ComponentMeta; + +export const Basic: ComponentStory = args => ( + +
+ +
+
+); + +Basic.args = { + tableName: 'example_table', + loading: false, + hasError: false, + columnList: exampleColumns, +}; diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.test.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.test.tsx index b03b7cad92a6..6800594bd8eb 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.test.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.test.tsx @@ -18,24 +18,124 @@ */ import React from 'react'; import { render, screen } from 'spec/helpers/testing-library'; -import DatasetPanel from 'src/views/CRUD/data/dataset/AddDataset/DatasetPanel'; +import DatasetPanel, { + REFRESHING, + ALT_LOADING, + tableColumnDefinition, + COLUMN_TITLE, +} from './DatasetPanel'; +import { exampleColumns } from './fixtures'; +import { + SELECT_MESSAGE, + CREATE_MESSAGE, + VIEW_DATASET_MESSAGE, + SELECT_TABLE_TITLE, + NO_COLUMNS_TITLE, + NO_COLUMNS_DESCRIPTION, + ERROR_TITLE, + ERROR_DESCRIPTION, +} from './MessageContent'; + +jest.mock( + 'src/components/Icons/Icon', + () => + ({ fileName }: { fileName: string }) => + , +); describe('DatasetPanel', () => { it('renders a blank state DatasetPanel', () => { - render(); + render(); const blankDatasetImg = screen.getByRole('img', { name: /empty/i }); - const blankDatasetTitle = screen.getByText(/select dataset source/i); - const blankDatasetDescription = screen.getByText( - /datasets can be created from database tables or sql queries\. select a database table to the left or to open sql lab\. from there you can save the query as a dataset\./i, - ); + expect(blankDatasetImg).toBeVisible(); + const blankDatasetTitle = screen.getByText(SELECT_TABLE_TITLE); + expect(blankDatasetTitle).toBeVisible(); + const blankDatasetDescription1 = screen.getByText(SELECT_MESSAGE, { + exact: false, + }); + expect(blankDatasetDescription1).toBeVisible(); + const blankDatasetDescription2 = screen.getByText(VIEW_DATASET_MESSAGE, { + exact: false, + }); + expect(blankDatasetDescription2).toBeVisible(); const sqlLabLink = screen.getByRole('button', { - name: /create dataset from sql query/i, + name: CREATE_MESSAGE, }); + expect(sqlLabLink).toBeVisible(); + }); + + it('renders a no columns screen', () => { + render( + , + ); + const blankDatasetImg = screen.getByRole('img', { name: /empty/i }); expect(blankDatasetImg).toBeVisible(); + const noColumnsTitle = screen.getByText(NO_COLUMNS_TITLE); + expect(noColumnsTitle).toBeVisible(); + const noColumnsDescription = screen.getByText(NO_COLUMNS_DESCRIPTION); + expect(noColumnsDescription).toBeVisible(); + }); + + it('renders a loading screen', () => { + render( + , + ); + + const blankDatasetImg = screen.getByAltText(ALT_LOADING); + expect(blankDatasetImg).toBeVisible(); + const blankDatasetTitle = screen.getByText(REFRESHING); expect(blankDatasetTitle).toBeVisible(); - expect(blankDatasetDescription).toBeVisible(); - expect(sqlLabLink).toBeVisible(); + }); + + it('renders an error screen', () => { + render( + , + ); + + const errorTitle = screen.getByText(ERROR_TITLE); + expect(errorTitle).toBeVisible(); + const errorDescription = screen.getByText(ERROR_DESCRIPTION); + expect(errorDescription).toBeVisible(); + }); + + it('renders a table with columns displayed', async () => { + const tableName = 'example_name'; + render( + , + ); + expect(await screen.findByText(tableName)).toBeVisible(); + expect(screen.getByText(COLUMN_TITLE)).toBeVisible(); + expect( + screen.getByText(tableColumnDefinition[0].title as string), + ).toBeInTheDocument(); + expect( + screen.getByText(tableColumnDefinition[1].title as string), + ).toBeInTheDocument(); + exampleColumns.forEach(row => { + expect(screen.getByText(row.name)).toBeInTheDocument(); + expect(screen.getByText(row.type)).toBeInTheDocument(); + }); }); }); diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.tsx new file mode 100644 index 000000000000..2a5e12cea888 --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/DatasetPanel.tsx @@ -0,0 +1,237 @@ +/** + * 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 React from 'react'; +import { supersetTheme, t, styled } from '@superset-ui/core'; +import Icons from 'src/components/Icons'; +import Table, { ColumnsType, TableSize } from 'src/components/Table'; +import { alphabeticalSort } from 'src/components/Table/sorters'; +// @ts-ignore +import LOADING_GIF from 'src/assets/images/loading.gif'; +import { ITableColumn } from './types'; +import MessageContent from './MessageContent'; + +/** + * Enum defining CSS position options + */ +enum EPosition { + ABSOLUTE = 'absolute', + RELATIVE = 'relative', +} + +/** + * Interface for StyledHeader + */ +interface StyledHeaderProps { + /** + * Determine the CSS positioning type + * Vertical centering of loader, No columns screen, and select table screen + * gets offset when the header position is relative and needs to be absolute, but table + * needs this positioned relative to render correctly + */ + position: EPosition; +} + +const LOADER_WIDTH = 200; +const SPINNER_WIDTH = 120; +const HALF = 0.5; +const MARGIN_MULTIPLIER = 3; + +const StyledHeader = styled.div` + position: ${(props: StyledHeaderProps) => props.position}; + margin: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; + margin-top: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px; + font-size: ${({ theme }) => theme.gridUnit * 6}px; + font-weight: ${({ theme }) => theme.typography.weights.medium}; + padding-bottom: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + .anticon:first-of-type { + margin-right: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px; + } + + .anticon:nth-of-type(2) { + margin-left: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px; + } +`; + +const StyledTitle = styled.div` + margin-left: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; + margin-bottom: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; + font-weight: ${({ theme }) => theme.typography.weights.bold}; +`; + +const LoaderContainer = styled.div` + padding: ${({ theme }) => theme.gridUnit * 8}px + ${({ theme }) => theme.gridUnit * 6}px; + + display: flex; + align-items: center; + justify-content: center; + height: 100%; +`; + +const StyledLoader = styled.div` + max-width: 50%; + width: ${LOADER_WIDTH}px; + + img { + width: ${SPINNER_WIDTH}px; + margin-left: ${(LOADER_WIDTH - SPINNER_WIDTH) * HALF}px; + } + + div { + width: 100%; + margin-top: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; + text-align: center; + font-weight: ${({ theme }) => theme.typography.weights.normal}; + font-size: ${({ theme }) => theme.typography.sizes.l}px; + color: ${({ theme }) => theme.colors.grayscale.light1}; + } +`; + +const TableContainer = styled.div` + position: relative; + margin: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; + overflow: scroll; + height: calc(100% - ${({ theme }) => theme.gridUnit * 36}px); +`; + +const StyledTable = styled(Table)` + position: absolute; + left: 0; + top: 0; + bottom: 0; + right: 0; +`; + +export const REFRESHING = t('Refreshing columns'); +export const COLUMN_TITLE = t('Table columns'); +export const ALT_LOADING = t('Loading'); + +const pageSizeOptions = ['5', '10', '15', '25']; + +// Define the columns for Table instance +export const tableColumnDefinition: ColumnsType = [ + { + title: 'Column Name', + dataIndex: 'name', + key: 'name', + sorter: (a: ITableColumn, b: ITableColumn) => + alphabeticalSort('name', a, b), + }, + { + title: 'Datatype', + dataIndex: 'type', + key: 'type', + width: '100px', + sorter: (a: ITableColumn, b: ITableColumn) => + alphabeticalSort('type', a, b), + }, +]; + +/** + * Props interface for DatasetPanel + */ +export interface IDatasetPanelProps { + /** + * Name of the database table + */ + tableName?: string | null; + /** + * Array of ITableColumn instances with name and type attributes + */ + columnList: ITableColumn[]; + /** + * Boolean indicating if there is an error state + */ + hasError: boolean; + /** + * Boolean indicating if the component is in a loading state + */ + loading: boolean; +} + +const DatasetPanel = ({ + tableName, + columnList, + loading, + hasError, +}: IDatasetPanelProps) => { + const hasColumns = columnList?.length > 0 ?? false; + + let component; + if (loading) { + component = ( + + + {ALT_LOADING} +
{REFRESHING}
+
+
+ ); + } else if (tableName && hasColumns && !hasError) { + component = ( + <> + {COLUMN_TITLE} + + + + + ); + } else { + component = ( + + ); + } + + return ( + <> + {tableName && ( + + {tableName && ( + + )} + {tableName} + + )} + {component} + + ); +}; + +export default DatasetPanel; diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/MessageContent.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/MessageContent.tsx new file mode 100644 index 000000000000..5d0ef5eda736 --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/MessageContent.tsx @@ -0,0 +1,107 @@ +/** + * 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 React from 'react'; +import { t, styled } from '@superset-ui/core'; +import { EmptyStateBig } from 'src/components/EmptyState'; + +const StyledContainer = styled.div` + padding: ${({ theme }) => theme.gridUnit * 8}px + ${({ theme }) => theme.gridUnit * 6}px; + + display: flex; + align-items: center; + justify-content: center; + height: 100%; +`; + +const StyledEmptyStateBig = styled(EmptyStateBig)` + max-width: 50%; + + p { + width: ${({ theme }) => theme.gridUnit * 115}px; + } +`; + +export const SELECT_MESSAGE = t( + 'Datasets can be created from database tables or SQL queries. Select a database table to the left or ', +); +export const CREATE_MESSAGE = t('create dataset from SQL query'); +export const VIEW_DATASET_MESSAGE = t( + ' to open SQL Lab. From there you can save the query as a dataset.', +); + +const renderEmptyDescription = () => ( + <> + {SELECT_MESSAGE} + { + window.location.href = `/superset/sqllab`; + }} + tabIndex={0} + > + {CREATE_MESSAGE} + + {VIEW_DATASET_MESSAGE} + +); + +export const SELECT_TABLE_TITLE = t('Select dataset source'); +export const NO_COLUMNS_TITLE = t('No table columns'); +export const NO_COLUMNS_DESCRIPTION = t( + 'This database table does not contain any data. Please select a different table.', +); +export const ERROR_TITLE = t('An Error Occurred'); +export const ERROR_DESCRIPTION = t( + 'Unable to load columns for the selected table. Please select a different table.', +); + +interface MessageContentProps { + hasError: boolean; + tableName?: string | null; + hasColumns: boolean; +} + +export const MessageContent = (props: MessageContentProps) => { + const { hasError, tableName, hasColumns } = props; + let currentImage: string | undefined = 'empty-dataset.svg'; + let currentTitle = SELECT_TABLE_TITLE; + let currentDescription = renderEmptyDescription(); + if (hasError) { + currentTitle = ERROR_TITLE; + currentDescription = <>{ERROR_DESCRIPTION}; + currentImage = undefined; + } else if (tableName && !hasColumns) { + currentImage = 'no-columns.svg'; + currentTitle = NO_COLUMNS_TITLE; + currentDescription = <>{NO_COLUMNS_DESCRIPTION}; + } + return ( + + + + ); +}; + +export default MessageContent; diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/fixtures.ts b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/fixtures.ts new file mode 100644 index 000000000000..2199190c9906 --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/fixtures.ts @@ -0,0 +1,34 @@ +/** + * 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 { ITableColumn } from './types'; + +export const exampleColumns: ITableColumn[] = [ + { + name: 'name', + type: 'STRING', + }, + { + name: 'height_in_inches', + type: 'NUMBER', + }, + { + name: 'birth_date', + type: 'DATE', + }, +]; diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/index.tsx index d4065ba3596b..e390c781fd2e 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/index.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/index.tsx @@ -16,78 +16,112 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; -import { supersetTheme, t, styled } from '@superset-ui/core'; -import Icons from 'src/components/Icons'; -import { EmptyStateBig } from 'src/components/EmptyState'; +import React, { useEffect, useState, useRef } from 'react'; +import { SupersetClient } from '@superset-ui/core'; +import DatasetPanel from './DatasetPanel'; +import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types'; -type DatasetPanelProps = { - tableName?: string | null; -}; +/** + * Interface for the getTableMetadata API call + */ +interface IColumnProps { + /** + * Unique id of the database + */ + dbId: number; + /** + * Name of the table + */ + tableName: string; + /** + * Name of the schema + */ + schema: string; +} -const StyledEmptyStateBig = styled(EmptyStateBig)` - p { - width: ${({ theme }) => theme.gridUnit * 115}px; - } -`; +export interface IDatasetPanelWrapperProps { + /** + * Name of the database table + */ + tableName?: string | null; + /** + * Database ID + */ + dbId?: number; + /** + * The selected schema for the database + */ + schema?: string | null; + setHasColumns?: Function; +} -const StyledDatasetPanel = styled.div` - padding: ${({ theme }) => theme.gridUnit * 8}px - ${({ theme }) => theme.gridUnit * 6}px; +const DatasetPanelWrapper = ({ + tableName, + dbId, + schema, + setHasColumns, +}: IDatasetPanelWrapperProps) => { + const [columnList, setColumnList] = useState([]); + const [loading, setLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const tableNameRef = useRef(tableName); - .table-name { - font-size: ${({ theme }) => theme.gridUnit * 6}px; - font-weight: ${({ theme }) => theme.typography.weights.medium}; - padding-bottom: ${({ theme }) => theme.gridUnit * 20}px; - margin: 0; + const getTableMetadata = async (props: IColumnProps) => { + const { dbId, tableName, schema } = props; + setLoading(true); + setHasColumns?.(false); + const path = `/api/v1/database/${dbId}/table/${tableName}/${schema}/`; + try { + const response = await SupersetClient.get({ + endpoint: path, + }); - .anticon:first-of-type { - margin-right: ${({ theme }) => theme.gridUnit * 4}px; + if (isIDatabaseTable(response?.json)) { + const table: IDatabaseTable = response.json as IDatabaseTable; + /** + * The user is able to click other table columns while the http call for last selected table column is made + * This check ensures we process the response that matches the last selected table name and ignore the others + */ + if (table.name === tableNameRef.current) { + setColumnList(table.columns); + setHasColumns?.(table.columns.length > 0); + setHasError(false); + } + } else { + setColumnList([]); + setHasColumns?.(false); + setHasError(true); + // eslint-disable-next-line no-console + console.error( + `The API response from ${path} does not match the IDatabaseTable interface.`, + ); + } + } catch (error) { + setColumnList([]); + setHasColumns?.(false); + setHasError(true); + } finally { + setLoading(false); } + }; - .anticon:nth-of-type(2) { - margin-left: ${({ theme }) => theme.gridUnit * 4}px; + useEffect(() => { + tableNameRef.current = tableName; + if (tableName && schema && dbId) { + getTableMetadata({ tableName, dbId, schema }); } - } - - span { - font-weight: ${({ theme }) => theme.typography.weights.bold}; - } -`; + // getTableMetadata is a const and should not be independency array + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [tableName, dbId, schema]); -const renderEmptyDescription = () => ( - <> - {t( - 'Datasets can be created from database tables or SQL queries. Select a database table to the left or ', - )} - { - window.location.href = `/superset/sqllab`; - }} - tabIndex={0} - > - {t('create dataset from SQL query')} - - {t(' to open SQL Lab. From there you can save the query as a dataset.')} - -); - -const DatasetPanel = ({ tableName }: DatasetPanelProps) => - tableName ? ( - -
- - {tableName} -
- {t('Table columns')} -
- ) : ( - ); +}; -export default DatasetPanel; +export default DatasetPanelWrapper; diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/types.ts b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/types.ts new file mode 100644 index 000000000000..c2330f3f10a4 --- /dev/null +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/DatasetPanel/types.ts @@ -0,0 +1,92 @@ +/** + * 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. + */ + +/** + * Interface for table columns dataset + */ +export interface ITableColumn { + /** + * Name of the column + */ + name: string; + /** + * Datatype of the column + */ + type: string; +} + +/** + * Checks if a given item matches the ITableColumn interface + * @param item Object to check if it matches the ITableColumn interface + * @returns boolean true if matches interface + */ +export const isITableColumn = (item: any): boolean => { + let match = true; + const BASE_ERROR = + 'The object provided to isITableColumn does match the interface.'; + if (typeof item?.name !== 'string') { + match = false; + // eslint-disable-next-line no-console + console.error( + `${BASE_ERROR} The property 'name' is required and must be a string`, + ); + } + if (match && typeof item?.type !== 'string') { + match = false; + // eslint-disable-next-line no-console + console.error( + `${BASE_ERROR} The property 'type' is required and must be a string`, + ); + } + return match; +}; + +export interface IDatabaseTable { + name: string; + columns: ITableColumn[]; +} + +/** + * Checks if a given item matches the isIDatabsetTable interface + * @param item Object to check if it matches the isIDatabsetTable interface + * @returns boolean true if matches interface + */ +export const isIDatabaseTable = (item: any): boolean => { + let match = true; + if (typeof item?.name !== 'string') { + match = false; + } + if (match && !Array.isArray(item.columns)) { + match = false; + } + if (match && item.columns.length > 0) { + const invalid = item.columns.some((column: any, index: number) => { + const valid = isITableColumn(column); + if (!valid) { + // eslint-disable-next-line no-console + console.error( + `The provided object does not match the IDatabaseTable interface. columns[${index}] is invalid and does not match the ITableColumn interface`, + ); + } + return !valid; + }); + match = !invalid; + } + return match; +}; diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/Footer.test.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/Footer.test.tsx index 44724ad59787..bb51c4de8644 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/Footer.test.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/Footer.test.tsx @@ -36,6 +36,7 @@ const mockPropsWithDataset = { dataset_name: 'Untitled', table_name: 'real_info', }, + hasColumns: true, }; describe('Footer', () => { diff --git a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/index.tsx b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/index.tsx index 2148f114cde5..7e08f3b9dfcc 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/index.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/AddDataset/Footer/index.tsx @@ -36,6 +36,7 @@ interface FooterProps { addDangerToast: () => void; datasetObject?: Partial | null; onDatasetAdd?: (dataset: DatasetObject) => void; + hasColumns?: boolean; } const INPUT_FIELDS = ['db', 'schema', 'table_name']; @@ -46,7 +47,12 @@ const LOG_ACTIONS = [ LOG_ACTIONS_DATASET_CREATION_TABLE_CANCELLATION, ]; -function Footer({ url, datasetObject, addDangerToast }: FooterProps) { +function Footer({ + url, + datasetObject, + addDangerToast, + hasColumns = false, +}: FooterProps) { const { createResource } = useSingleViewResource>( 'dataset', t('dataset'), @@ -107,7 +113,7 @@ function Footer({ url, datasetObject, addDangerToast }: FooterProps) {