diff --git a/superset-frontend/.storybook/main.js b/superset-frontend/.storybook/main.js
index b8f15b569f3d9..814e53cf58411 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 0000000000000..2fc8fe0661bfe
--- /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 3f5d586cf8d92..982cace6672fb 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 0000000000000..8a7fd7d6438fe
--- /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 b03b7cad92a63..6800594bd8eb1 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 0000000000000..2a5e12cea888d
--- /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 = (
+
+
+
+
{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 0000000000000..5d0ef5eda736e
--- /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 0000000000000..2199190c99067
--- /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 d4065ba3596bc..e390c781fd2e7 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 0000000000000..c2330f3f10a48
--- /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 44724ad597870..bb51c4de86447 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 2148f114cde57..7e08f3b9dfcc1 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) {