feat: Flow for tables that already have a dataset (#22136)

This commit is contained in:
Lyndsi Kay Williams 2022-12-05 15:43:54 -06:00 committed by GitHub
parent 96de314c0c
commit 04b7a26365
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 366 additions and 82 deletions

View File

@ -24,7 +24,7 @@ import DatasetPanel, {
tableColumnDefinition, tableColumnDefinition,
COLUMN_TITLE, COLUMN_TITLE,
} from './DatasetPanel'; } from './DatasetPanel';
import { exampleColumns } from './fixtures'; import { exampleColumns, exampleDataset } from './fixtures';
import { import {
SELECT_MESSAGE, SELECT_MESSAGE,
CREATE_MESSAGE, CREATE_MESSAGE,
@ -44,7 +44,7 @@ jest.mock(
); );
describe('DatasetPanel', () => { describe('DatasetPanel', () => {
it('renders a blank state DatasetPanel', () => { test('renders a blank state DatasetPanel', () => {
render(<DatasetPanel hasError={false} columnList={[]} loading={false} />); render(<DatasetPanel hasError={false} columnList={[]} loading={false} />);
const blankDatasetImg = screen.getByRole('img', { name: /empty/i }); const blankDatasetImg = screen.getByRole('img', { name: /empty/i });
@ -65,7 +65,7 @@ describe('DatasetPanel', () => {
expect(sqlLabLink).toBeVisible(); expect(sqlLabLink).toBeVisible();
}); });
it('renders a no columns screen', () => { test('renders a no columns screen', () => {
render( render(
<DatasetPanel <DatasetPanel
tableName="Name" tableName="Name"
@ -83,7 +83,7 @@ describe('DatasetPanel', () => {
expect(noColumnsDescription).toBeVisible(); expect(noColumnsDescription).toBeVisible();
}); });
it('renders a loading screen', () => { test('renders a loading screen', () => {
render( render(
<DatasetPanel <DatasetPanel
tableName="Name" tableName="Name"
@ -99,7 +99,7 @@ describe('DatasetPanel', () => {
expect(blankDatasetTitle).toBeVisible(); expect(blankDatasetTitle).toBeVisible();
}); });
it('renders an error screen', () => { test('renders an error screen', () => {
render( render(
<DatasetPanel <DatasetPanel
tableName="Name" tableName="Name"
@ -115,7 +115,7 @@ describe('DatasetPanel', () => {
expect(errorDescription).toBeVisible(); expect(errorDescription).toBeVisible();
}); });
it('renders a table with columns displayed', async () => { test('renders a table with columns displayed', async () => {
const tableName = 'example_name'; const tableName = 'example_name';
render( render(
<DatasetPanel <DatasetPanel
@ -138,4 +138,23 @@ describe('DatasetPanel', () => {
expect(screen.getByText(row.type)).toBeInTheDocument(); expect(screen.getByText(row.type)).toBeInTheDocument();
}); });
}); });
test('renders an info banner if table already has a dataset', async () => {
render(
<DatasetPanel
tableName="example_table"
hasError={false}
columnList={exampleColumns}
loading={false}
datasets={exampleDataset}
/>,
);
// This is text in the info banner
expect(
await screen.findByText(
/this table already has a dataset associated with it. you can only associate one dataset with a table./i,
),
).toBeVisible();
});
}); });

View File

@ -17,12 +17,14 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import { supersetTheme, t, styled } from '@superset-ui/core'; import { t, styled, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
import Alert from 'src/components/Alert';
import Table, { ColumnsType, TableSize } from 'src/components/Table'; import Table, { ColumnsType, TableSize } from 'src/components/Table';
import { alphabeticalSort } from 'src/components/Table/sorters'; import { alphabeticalSort } from 'src/components/Table/sorters';
// @ts-ignore // @ts-ignore
import LOADING_GIF from 'src/assets/images/loading.gif'; import LOADING_GIF from 'src/assets/images/loading.gif';
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
import { ITableColumn } from './types'; import { ITableColumn } from './types';
import MessageContent from './MessageContent'; import MessageContent from './MessageContent';
@ -53,43 +55,56 @@ const HALF = 0.5;
const MARGIN_MULTIPLIER = 3; const MARGIN_MULTIPLIER = 3;
const StyledHeader = styled.div<StyledHeaderProps>` const StyledHeader = styled.div<StyledHeaderProps>`
${({ theme }) => `
position: ${(props: StyledHeaderProps) => props.position}; position: ${(props: StyledHeaderProps) => props.position};
margin: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; margin: ${theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px
margin-top: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px; ${theme.gridUnit * MARGIN_MULTIPLIER}px
font-size: ${({ theme }) => theme.gridUnit * 6}px; ${theme.gridUnit * MARGIN_MULTIPLIER}px
font-weight: ${({ theme }) => theme.typography.weights.medium}; ${theme.gridUnit * (MARGIN_MULTIPLIER + 3)}px;
padding-bottom: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; font-size: ${theme.gridUnit * 6}px;
font-weight: ${theme.typography.weights.medium};
padding-bottom: ${theme.gridUnit * MARGIN_MULTIPLIER}px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
.anticon:first-of-type { .anticon:first-of-type {
margin-right: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px; margin-right: ${theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
} }
.anticon:nth-of-type(2) { .anticon:nth-of-type(2) {
margin-left: ${({ theme }) => theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px; margin-left: ${theme.gridUnit * (MARGIN_MULTIPLIER + 1)}px;
} `}
`; `;
const StyledTitle = styled.div` const StyledTitle = styled.div`
margin-left: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; ${({ theme }) => `
margin-bottom: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; margin-left: ${theme.gridUnit * (MARGIN_MULTIPLIER + 3)}px;
font-weight: ${({ theme }) => theme.typography.weights.bold}; margin-bottom: ${theme.gridUnit * MARGIN_MULTIPLIER}px;
font-weight: ${theme.typography.weights.bold};
`}
`; `;
const LoaderContainer = styled.div` const LoaderContainer = styled.div`
padding: ${({ theme }) => theme.gridUnit * 8}px ${({ theme }) => `
${({ theme }) => theme.gridUnit * 6}px; padding: ${theme.gridUnit * 8}px
${theme.gridUnit * 6}px;
box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 100%; height: 100%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
`}
`; `;
const StyledLoader = styled.div` const StyledLoader = styled.div`
${({ theme }) => `
max-width: 50%; max-width: 50%;
width: ${LOADER_WIDTH}px; width: ${LOADER_WIDTH}px;
@ -100,19 +115,23 @@ const StyledLoader = styled.div`
div { div {
width: 100%; width: 100%;
margin-top: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; margin-top: ${theme.gridUnit * MARGIN_MULTIPLIER}px;
text-align: center; text-align: center;
font-weight: ${({ theme }) => theme.typography.weights.normal}; font-weight: ${theme.typography.weights.normal};
font-size: ${({ theme }) => theme.typography.sizes.l}px; font-size: ${theme.typography.sizes.l}px;
color: ${({ theme }) => theme.colors.grayscale.light1}; color: ${theme.colors.grayscale.light1};
} }
`}
`; `;
const TableContainer = styled.div` const TableContainer = styled.div`
${({ theme }) => `
position: relative; position: relative;
margin: ${({ theme }) => theme.gridUnit * MARGIN_MULTIPLIER}px; margin: ${theme.gridUnit * MARGIN_MULTIPLIER}px;
margin-left: ${theme.gridUnit * (MARGIN_MULTIPLIER + 3)}px;
overflow: scroll; overflow: scroll;
height: calc(100% - ${({ theme }) => theme.gridUnit * 36}px); height: calc(100% - ${theme.gridUnit * 36}px);
`}
`; `;
const StyledTable = styled(Table)` const StyledTable = styled(Table)`
@ -123,6 +142,26 @@ const StyledTable = styled(Table)`
right: 0; right: 0;
`; `;
const StyledAlert = styled(Alert)`
${({ theme }) => `
border: 1px solid ${theme.colors.info.base};
padding: ${theme.gridUnit * 4}px;
margin: ${theme.gridUnit * 6}px ${theme.gridUnit * 6}px
${theme.gridUnit * 8}px;
.view-dataset-button {
position: absolute;
top: ${theme.gridUnit * 4}px;
right: ${theme.gridUnit * 4}px;
font-weight: ${theme.typography.weights.normal};
&:hover {
color: ${theme.colors.secondary.dark3};
text-decoration: underline;
}
}
`}
`;
export const REFRESHING = t('Refreshing columns'); export const REFRESHING = t('Refreshing columns');
export const COLUMN_TITLE = t('Table columns'); export const COLUMN_TITLE = t('Table columns');
export const ALT_LOADING = t('Loading'); export const ALT_LOADING = t('Loading');
@ -168,19 +207,57 @@ export interface IDatasetPanelProps {
* Boolean indicating if the component is in a loading state * Boolean indicating if the component is in a loading state
*/ */
loading: boolean; loading: boolean;
datasets?: DatasetObject[] | undefined;
} }
const EXISTING_DATASET_DESCRIPTION = t(
'This table already has a dataset associated with it. You can only associate one dataset with a table.\n',
);
const VIEW_DATASET = t('View Dataset');
const renderExistingDatasetAlert = (dataset?: DatasetObject) => (
<StyledAlert
closable={false}
type="info"
showIcon
message={t('This table already has a dataset')}
description={
<>
{EXISTING_DATASET_DESCRIPTION}
<span
role="button"
onClick={() => {
window.open(
dataset?.explore_url,
'_blank',
'noreferrer noopener popup=false',
);
}}
tabIndex={0}
className="view-dataset-button"
>
{VIEW_DATASET}
</span>
</>
}
/>
);
const DatasetPanel = ({ const DatasetPanel = ({
tableName, tableName,
columnList, columnList,
loading, loading,
hasError, hasError,
datasets,
}: IDatasetPanelProps) => { }: IDatasetPanelProps) => {
const theme = useTheme();
const hasColumns = columnList?.length > 0 ?? false; const hasColumns = columnList?.length > 0 ?? false;
const datasetNames = datasets?.map(dataset => dataset.table_name);
let component; let component;
let loader;
if (loading) { if (loading) {
component = ( loader = (
<LoaderContainer> <LoaderContainer>
<StyledLoader> <StyledLoader>
<img alt={ALT_LOADING} src={LOADING_GIF} /> <img alt={ALT_LOADING} src={LOADING_GIF} />
@ -188,48 +265,58 @@ const DatasetPanel = ({
</StyledLoader> </StyledLoader>
</LoaderContainer> </LoaderContainer>
); );
} else if (tableName && hasColumns && !hasError) { }
component = ( if (!loading) {
<> if (!loading && tableName && hasColumns && !hasError) {
<StyledTitle>{COLUMN_TITLE}</StyledTitle> component = (
<TableContainer> <>
<StyledTable <StyledTitle>{COLUMN_TITLE}</StyledTitle>
loading={loading} <TableContainer>
size={TableSize.SMALL} <StyledTable
columns={tableColumnDefinition} loading={loading}
data={columnList} size={TableSize.SMALL}
pageSizeOptions={pageSizeOptions} columns={tableColumnDefinition}
defaultPageSize={10} data={columnList}
/> pageSizeOptions={pageSizeOptions}
</TableContainer> defaultPageSize={10}
</> />
); </TableContainer>
} else { </>
component = ( );
<MessageContent } else {
hasColumns={hasColumns} component = (
hasError={hasError} <MessageContent
tableName={tableName} hasColumns={hasColumns}
/> hasError={hasError}
); tableName={tableName}
/>
);
}
} }
return ( return (
<> <>
{tableName && ( {tableName && (
<StyledHeader <>
position={ {datasetNames?.includes(tableName) &&
!loading && hasColumns ? EPosition.RELATIVE : EPosition.ABSOLUTE renderExistingDatasetAlert(
} datasets?.find(dataset => dataset.table_name === tableName),
title={tableName || ''} )}
> <StyledHeader
{tableName && ( position={
<Icons.Table iconColor={supersetTheme.colors.grayscale.base} /> !loading && hasColumns ? EPosition.RELATIVE : EPosition.ABSOLUTE
)} }
{tableName} title={tableName || ''}
</StyledHeader> >
{tableName && (
<Icons.Table iconColor={theme.colors.grayscale.base} />
)}
{tableName}
</StyledHeader>
</>
)} )}
{component} {component}
{loader}
</> </>
); );
}; };

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
import { ITableColumn } from './types'; import { ITableColumn } from './types';
export const exampleColumns: ITableColumn[] = [ export const exampleColumns: ITableColumn[] = [
@ -32,3 +33,16 @@ export const exampleColumns: ITableColumn[] = [
type: 'DATE', type: 'DATE',
}, },
]; ];
export const exampleDataset: DatasetObject[] = [
{
db: {
id: 1,
database_name: 'test_database',
owners: [1],
},
schema: 'test_schema',
dataset_name: 'example_dataset',
table_name: 'example_table',
},
];

View File

@ -18,6 +18,7 @@
*/ */
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { SupersetClient } from '@superset-ui/core'; import { SupersetClient } from '@superset-ui/core';
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
import DatasetPanel from './DatasetPanel'; import DatasetPanel from './DatasetPanel';
import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types'; import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
@ -53,6 +54,7 @@ export interface IDatasetPanelWrapperProps {
*/ */
schema?: string | null; schema?: string | null;
setHasColumns?: Function; setHasColumns?: Function;
datasets?: DatasetObject[] | undefined;
} }
const DatasetPanelWrapper = ({ const DatasetPanelWrapper = ({
@ -60,6 +62,7 @@ const DatasetPanelWrapper = ({
dbId, dbId,
schema, schema,
setHasColumns, setHasColumns,
datasets,
}: IDatasetPanelWrapperProps) => { }: IDatasetPanelWrapperProps) => {
const [columnList, setColumnList] = useState<ITableColumn[]>([]); const [columnList, setColumnList] = useState<ITableColumn[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -110,7 +113,7 @@ const DatasetPanelWrapper = ({
if (tableName && schema && dbId) { if (tableName && schema && dbId) {
getTableMetadata({ tableName, dbId, schema }); getTableMetadata({ tableName, dbId, schema });
} }
// getTableMetadata is a const and should not be independency array // getTableMetadata is a const and should not be in dependency array
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [tableName, dbId, schema]); }, [tableName, dbId, schema]);
@ -120,6 +123,7 @@ const DatasetPanelWrapper = ({
hasError={hasError} hasError={hasError}
loading={loading} loading={loading}
tableName={tableName} tableName={tableName}
datasets={datasets}
/> />
); );
}; };

View File

@ -40,7 +40,7 @@ const mockPropsWithDataset = {
}; };
describe('Footer', () => { describe('Footer', () => {
it('renders a Footer with a cancel button and a disabled create button', () => { test('renders a Footer with a cancel button and a disabled create button', () => {
render(<Footer {...mockedProps} />, { useRedux: true }); render(<Footer {...mockedProps} />, { useRedux: true });
const saveButton = screen.getByRole('button', { const saveButton = screen.getByRole('button', {
@ -55,7 +55,7 @@ describe('Footer', () => {
expect(createButton).toBeDisabled(); expect(createButton).toBeDisabled();
}); });
it('renders a Create Dataset button when a table is selected', () => { test('renders a Create Dataset button when a table is selected', () => {
render(<Footer {...mockPropsWithDataset} />, { useRedux: true }); render(<Footer {...mockPropsWithDataset} />, { useRedux: true });
const createButton = screen.getByRole('button', { const createButton = screen.getByRole('button', {
@ -64,4 +64,16 @@ describe('Footer', () => {
expect(createButton).toBeEnabled(); expect(createButton).toBeEnabled();
}); });
test('create button becomes disabled when table already has a dataset', () => {
render(<Footer datasets={['real_info']} {...mockPropsWithDataset} />, {
useRedux: true,
});
const createButton = screen.getByRole('button', {
name: /Create/i,
});
expect(createButton).toBeDisabled();
});
}); });

View File

@ -37,6 +37,7 @@ interface FooterProps {
datasetObject?: Partial<DatasetObject> | null; datasetObject?: Partial<DatasetObject> | null;
onDatasetAdd?: (dataset: DatasetObject) => void; onDatasetAdd?: (dataset: DatasetObject) => void;
hasColumns?: boolean; hasColumns?: boolean;
datasets?: (string | null | undefined)[] | undefined;
} }
const INPUT_FIELDS = ['db', 'schema', 'table_name']; const INPUT_FIELDS = ['db', 'schema', 'table_name'];
@ -52,6 +53,7 @@ function Footer({
datasetObject, datasetObject,
addDangerToast, addDangerToast,
hasColumns = false, hasColumns = false,
datasets,
}: FooterProps) { }: FooterProps) {
const { createResource } = useSingleViewResource<Partial<DatasetObject>>( const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
'dataset', 'dataset',
@ -108,16 +110,22 @@ function Footer({
} }
}; };
const CREATE_DATASET_TEXT = t('Create Dataset');
const disabledCheck =
!datasetObject?.table_name ||
!hasColumns ||
datasets?.includes(datasetObject?.table_name);
return ( return (
<> <>
<Button onClick={cancelButtonOnClick}>Cancel</Button> <Button onClick={cancelButtonOnClick}>Cancel</Button>
<Button <Button
buttonStyle="primary" buttonStyle="primary"
disabled={!datasetObject?.table_name || !hasColumns} disabled={disabledCheck}
tooltip={!datasetObject?.table_name ? tooltipText : undefined} tooltip={!datasetObject?.table_name ? tooltipText : undefined}
onClick={onSave} onClick={onSave}
> >
{t('Create Dataset')} {CREATE_DATASET_TEXT}
</Button> </Button>
</> </>
); );

View File

@ -209,6 +209,7 @@ test('searches for a table name', async () => {
useRedux: true, useRedux: true,
}); });
// Click 'test-postgres' database to access schemas
const databaseSelect = screen.getByRole('combobox', { const databaseSelect = screen.getByRole('combobox', {
name: /select database or type database name/i, name: /select database or type database name/i,
}); });
@ -221,6 +222,7 @@ test('searches for a table name', async () => {
await waitFor(() => expect(schemaSelect).toBeEnabled()); await waitFor(() => expect(schemaSelect).toBeEnabled());
// Click 'public' schema to access tables
userEvent.click(schemaSelect); userEvent.click(schemaSelect);
userEvent.click(screen.getAllByText('public')[1]); userEvent.click(screen.getAllByText('public')[1]);
@ -238,3 +240,46 @@ test('searches for a table name', async () => {
expect(screen.queryByText('Sheet3')).not.toBeInTheDocument(); expect(screen.queryByText('Sheet3')).not.toBeInTheDocument();
}); });
}); });
test('renders a warning icon when a table name has a pre-existing dataset', async () => {
render(
<LeftPanel
setDataset={mockFun}
schema="schema_a"
dbId={1}
datasets={['Sheet2']}
/>,
{
useRedux: true,
},
);
// Click 'test-postgres' database to access schemas
const databaseSelect = screen.getByRole('combobox', {
name: /select database or type database name/i,
});
userEvent.click(databaseSelect);
userEvent.click(await screen.findByText('test-postgres'));
const schemaSelect = screen.getByRole('combobox', {
name: /select schema or type schema name/i,
});
await waitFor(() => expect(schemaSelect).toBeEnabled());
// Warning icon should not show yet
expect(
screen.queryByRole('img', { name: 'warning' }),
).not.toBeInTheDocument();
// Click 'public' schema to access tables
userEvent.click(schemaSelect);
userEvent.click(screen.getAllByText('public')[1]);
await waitFor(() => {
expect(screen.getByText('Sheet2')).toBeInTheDocument();
});
// Sheet2 should now show the warning icon
expect(screen.getByRole('img', { name: 'warning' })).toBeVisible();
});

View File

@ -17,7 +17,14 @@
* under the License. * under the License.
*/ */
import React, { useEffect, useState, SetStateAction, Dispatch } from 'react'; import React, { useEffect, useState, SetStateAction, Dispatch } from 'react';
import { SupersetClient, t, styled } from '@superset-ui/core'; import {
SupersetClient,
t,
styled,
css,
useTheme,
logging,
} from '@superset-ui/core';
import { Input } from 'src/components/Input'; import { Input } from 'src/components/Input';
import { Form } from 'src/components/Form'; import { Form } from 'src/components/Form';
import Icons from 'src/components/Icons'; import Icons from 'src/components/Icons';
@ -36,6 +43,7 @@ interface LeftPanelProps {
setDataset: Dispatch<SetStateAction<object>>; setDataset: Dispatch<SetStateAction<object>>;
schema?: string | null | undefined; schema?: string | null | undefined;
dbId?: number; dbId?: number;
datasets?: (string | null | undefined)[] | undefined;
} }
const SearchIcon = styled(Icons.Search)` const SearchIcon = styled(Icons.Search)`
@ -78,6 +86,7 @@ const LeftPanelStyle = styled.div`
top: ${theme.gridUnit * 92.25}px; top: ${theme.gridUnit * 92.25}px;
left: ${theme.gridUnit * 3.25}px; left: ${theme.gridUnit * 3.25}px;
right: 0; right: 0;
.options { .options {
cursor: pointer; cursor: pointer;
padding: ${theme.gridUnit * 1.75}px; padding: ${theme.gridUnit * 1.75}px;
@ -86,6 +95,7 @@ const LeftPanelStyle = styled.div`
background-color: ${theme.colors.grayscale.light4} background-color: ${theme.colors.grayscale.light4}
} }
} }
.options-highlighted { .options-highlighted {
cursor: pointer; cursor: pointer;
padding: ${theme.gridUnit * 1.75}px; padding: ${theme.gridUnit * 1.75}px;
@ -93,6 +103,12 @@ const LeftPanelStyle = styled.div`
background-color: ${theme.colors.primary.dark1}; background-color: ${theme.colors.primary.dark1};
color: ${theme.colors.grayscale.light5}; color: ${theme.colors.grayscale.light5};
} }
.options, .options-highlighted {
display: flex;
align-items: center;
justify-content: space-between;
}
} }
form > span[aria-label="refresh"] { form > span[aria-label="refresh"] {
position: absolute; position: absolute;
@ -124,7 +140,10 @@ export default function LeftPanel({
setDataset, setDataset,
schema, schema,
dbId, dbId,
datasets,
}: LeftPanelProps) { }: LeftPanelProps) {
const theme = useTheme();
const [tableOptions, setTableOptions] = useState<Array<TableOption>>([]); const [tableOptions, setTableOptions] = useState<Array<TableOption>>([]);
const [resetTables, setResetTables] = useState(false); const [resetTables, setResetTables] = useState(false);
const [loadTables, setLoadTables] = useState(false); const [loadTables, setLoadTables] = useState(false);
@ -166,9 +185,9 @@ export default function LeftPanel({
setResetTables(false); setResetTables(false);
setRefresh(false); setRefresh(false);
}) })
.catch(e => { .catch(error =>
console.log('error', e); logging.error('There was an error fetching tables', error),
}); );
}; };
const setSchema = (schema: string) => { const setSchema = (schema: string) => {
@ -212,21 +231,32 @@ export default function LeftPanel({
</div> </div>
); );
const SELECT_DATABASE_AND_SCHEMA_TEXT = t('Select database & schema');
const TABLE_LOADING_TEXT = t('Table loading');
const NO_TABLES_FOUND_TITLE = t('No database tables found');
const NO_TABLES_FOUND_DESCRIPTION = t('Try selecting a different schema');
const SELECT_DATABASE_TABLE_TEXT = t('Select database table');
const REFRESH_TABLE_LIST_TOOLTIP = t('Refresh table list');
const REFRESH_TABLES_TEXT = t('Refresh tables');
const SEARCH_TABLES_PLACEHOLDER_TEXT = t('Search tables');
return ( return (
<LeftPanelStyle> <LeftPanelStyle>
<p className="section-title db-schema">Select database & schema</p> <p className="section-title db-schema">
{SELECT_DATABASE_AND_SCHEMA_TEXT}
</p>
<DatabaseSelector <DatabaseSelector
handleError={addDangerToast} handleError={addDangerToast}
onDbChange={setDatabase} onDbChange={setDatabase}
onSchemaChange={setSchema} onSchemaChange={setSchema}
/> />
{loadTables && !refresh && Loader('Table loading')} {loadTables && !refresh && Loader(TABLE_LOADING_TEXT)}
{schema && !loadTables && !tableOptions.length && !searchVal && ( {schema && !loadTables && !tableOptions.length && !searchVal && (
<div className="emptystate"> <div className="emptystate">
<EmptyStateMedium <EmptyStateMedium
image="empty-table.svg" image="empty-table.svg"
title={t('No database tables found')} title={NO_TABLES_FOUND_TITLE}
description={t('Try selecting a different schema')} description={NO_TABLES_FOUND_DESCRIPTION}
/> />
</div> </div>
)} )}
@ -234,15 +264,15 @@ export default function LeftPanel({
{schema && (tableOptions.length > 0 || searchVal.length > 0) && ( {schema && (tableOptions.length > 0 || searchVal.length > 0) && (
<> <>
<Form> <Form>
<p className="table-title">Select database table</p> <p className="table-title">{SELECT_DATABASE_TABLE_TEXT}</p>
<RefreshLabel <RefreshLabel
onClick={() => { onClick={() => {
setLoadTables(true); setLoadTables(true);
setRefresh(true); setRefresh(true);
}} }}
tooltipContent={t('Refresh table list')} tooltipContent={REFRESH_TABLE_LIST_TOOLTIP}
/> />
{refresh && Loader('Refresh tables')} {refresh && Loader(REFRESH_TABLES_TEXT)}
{!refresh && ( {!refresh && (
<Input <Input
value={searchVal} value={searchVal}
@ -251,7 +281,7 @@ export default function LeftPanel({
setSearchVal(evt.target.value); setSearchVal(evt.target.value);
}} }}
className="table-form" className="table-form"
placeholder={t('Search tables')} placeholder={SEARCH_TABLES_PLACEHOLDER_TEXT}
allowClear allowClear
/> />
)} )}
@ -269,6 +299,19 @@ export default function LeftPanel({
onClick={() => setTable(option.value, i)} onClick={() => setTable(option.value, i)}
> >
{option.label} {option.label}
{datasets?.includes(option.value) && (
<Icons.Warning
iconColor={
selectedTable === i
? theme.colors.grayscale.light5
: theme.colors.info.base
}
iconSize="m"
css={css`
margin-right: ${theme.gridUnit * 6}px;
`}
/>
)}
</div> </div>
))} ))}
</div> </div>

View File

@ -16,7 +16,10 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useReducer, Reducer, useState } from 'react'; import React, { useReducer, Reducer, useEffect, useState } from 'react';
import { logging } from '@superset-ui/core';
import { UseGetDatasetsList } from 'src/views/CRUD/data/hooks';
import rison from 'rison';
import Header from './Header'; import Header from './Header';
import DatasetPanel from './DatasetPanel'; import DatasetPanel from './DatasetPanel';
import LeftPanel from './LeftPanel'; import LeftPanel from './LeftPanel';
@ -73,6 +76,36 @@ export default function AddDataset() {
Reducer<Partial<DatasetObject> | null, DSReducerActionType> Reducer<Partial<DatasetObject> | null, DSReducerActionType>
>(datasetReducer, null); >(datasetReducer, null);
const [hasColumns, setHasColumns] = useState(false); const [hasColumns, setHasColumns] = useState(false);
const [datasets, setDatasets] = useState<DatasetObject[]>([]);
const datasetNames = datasets.map(dataset => dataset.table_name);
const encodedSchema = dataset?.schema
? encodeURIComponent(dataset?.schema)
: undefined;
const queryParams = dataset?.schema
? rison.encode_uri({
filters: [
{ col: 'schema', opr: 'eq', value: encodedSchema },
{ col: 'sql', opr: 'dataset_is_null_or_empty', value: '!t' },
],
})
: undefined;
const getDatasetsList = async () => {
await UseGetDatasetsList(queryParams)
.then(json => {
setDatasets(json?.result);
})
.catch(error =>
logging.error('There was an error fetching dataset', error),
);
};
useEffect(() => {
if (dataset?.schema) {
getDatasetsList();
}
}, [dataset?.schema]);
const HeaderComponent = () => ( const HeaderComponent = () => (
<Header setDataset={setDataset} title={dataset?.table_name} /> <Header setDataset={setDataset} title={dataset?.table_name} />
@ -83,6 +116,7 @@ export default function AddDataset() {
setDataset={setDataset} setDataset={setDataset}
schema={dataset?.schema} schema={dataset?.schema}
dbId={dataset?.db?.id} dbId={dataset?.db?.id}
datasets={datasetNames}
/> />
); );
@ -92,11 +126,17 @@ export default function AddDataset() {
dbId={dataset?.db?.id} dbId={dataset?.db?.id}
schema={dataset?.schema} schema={dataset?.schema}
setHasColumns={setHasColumns} setHasColumns={setHasColumns}
datasets={datasets}
/> />
); );
const FooterComponent = () => ( const FooterComponent = () => (
<Footer url={prevUrl} datasetObject={dataset} hasColumns={hasColumns} /> <Footer
url={prevUrl}
datasetObject={dataset}
hasColumns={hasColumns}
datasets={datasetNames}
/>
); );
return ( return (

View File

@ -32,6 +32,7 @@ export interface DatasetObject {
schema?: string | null; schema?: string | null;
dataset_name: string; dataset_name: string;
table_name?: string | null; table_name?: string | null;
explore_url?: string;
} }
export interface DatasetReducerPayloadType { export interface DatasetReducerPayloadType {

View File

@ -82,6 +82,7 @@ export const StyledLayoutLeftPanel = styled.div`
export const StyledLayoutDatasetPanel = styled.div` export const StyledLayoutDatasetPanel = styled.div`
width: 100%; width: 100%;
position: relative;
`; `;
export const StyledLayoutRightPanel = styled.div` export const StyledLayoutRightPanel = styled.div`

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { SupersetClient, logging } from '@superset-ui/core';
type BaseQueryObject = { type BaseQueryObject = {
id: number; id: number;
@ -73,3 +74,12 @@ export function useQueryPreviewState<D extends BaseQueryObject = any>({
disableNext, disableNext,
}; };
} }
export const UseGetDatasetsList = (queryParams: string | undefined) =>
SupersetClient.get({
endpoint: `/api/v1/dataset/?q=${queryParams}`,
})
.then(({ json }) => json)
.catch(error =>
logging.error('There was an error fetching dataset', error),
);