feat: Enable new dataset creation flow II (#22835)

This commit is contained in:
Lyndsi Kay Williams 2023-02-01 09:49:25 -06:00 committed by GitHub
parent ebed50fd12
commit 260ac40b23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 237 additions and 376 deletions

View File

@ -186,27 +186,6 @@ test('should render a create dataset infobox', async () => {
expect(infoboxText).toBeVisible(); expect(infoboxText).toBeVisible();
}); });
test('should render a save dataset modal when "Create a dataset" is clicked', async () => {
const newProps = {
...props,
datasource: {
...datasource,
type: DatasourceType.Query,
},
};
render(<DatasourcePanel {...newProps} />, { useRedux: true, useDnd: true });
const createButton = await screen.findByRole('button', {
name: /create a dataset/i,
});
userEvent.click(createButton);
const saveDatasetModalTitle = screen.getByText(/save or overwrite dataset/i);
expect(saveDatasetModalTitle).toBeVisible();
});
test('should not render a save dataset modal when datasource is not query or dataset', async () => { test('should not render a save dataset modal when datasource is not query or dataset', async () => {
const newProps = { const newProps = {
...props, ...props,

View File

@ -231,7 +231,9 @@ const ColumnSelectPopover = ({
}, []); }, []);
const setDatasetAndClose = () => { const setDatasetAndClose = () => {
if (setDatasetModal) setDatasetModal(true); if (setDatasetModal) {
setDatasetModal(true);
}
onClose(); onClose();
}; };

View File

@ -337,10 +337,7 @@ export class ChartCreation extends React.PureComponent<
const isButtonDisabled = this.isBtnDisabled(); const isButtonDisabled = this.isBtnDisabled();
const datasetHelpText = this.state.canCreateDataset ? ( const datasetHelpText = this.state.canCreateDataset ? (
<span data-test="dataset-write"> <span data-test="dataset-write">
<Link <Link to="/dataset/add/" data-test="add-chart-new-dataset">
to="/tablemodelview/list/#create"
data-test="add-chart-new-dataset"
>
{t('Add a dataset')} {t('Add a dataset')}
</Link> </Link>
{` ${t('or')} `} {` ${t('or')} `}

View File

@ -21,6 +21,7 @@ import React, { useState, useMemo, useEffect } from 'react';
import rison from 'rison'; import rison from 'rison';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useQueryParams, BooleanParam } from 'use-query-params'; import { useQueryParams, BooleanParam } from 'use-query-params';
import { LocalStorageKeys, setItem } from 'src/utils/localStorageHelpers';
import Loading from 'src/components/Loading'; import Loading from 'src/components/Loading';
import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
@ -157,6 +158,9 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) {
refreshData(); refreshData();
addSuccessToast(t('Deleted: %s', dbName)); addSuccessToast(t('Deleted: %s', dbName));
// Delete user-selected db from local storage
setItem(LocalStorageKeys.db, null);
// Close delete modal // Close delete modal
setDatabaseCurrentlyDeleting(null); setDatabaseCurrentlyDeleting(null);
}, },

View File

@ -62,6 +62,14 @@ jest.mock('src/components/Icons/Icon', () => ({
StyledIcon: () => <span />, StyledIcon: () => <span />,
})); }));
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
const dbProps = { const dbProps = {
show: true, show: true,
database_name: 'my database', database_name: 'my database',

View File

@ -137,6 +137,7 @@ interface DatabaseModalProps {
show: boolean; show: boolean;
databaseId: number | undefined; // If included, will go into edit mode databaseId: number | undefined; // If included, will go into edit mode
dbEngine: string | undefined; // if included goto step 2 with engine already set dbEngine: string | undefined; // if included goto step 2 with engine already set
history?: any;
} }
export enum ActionType { export enum ActionType {
@ -521,6 +522,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
show, show,
databaseId, databaseId,
dbEngine, dbEngine,
history,
}) => { }) => {
const [db, setDB] = useReducer< const [db, setDB] = useReducer<
Reducer<Partial<DatabaseObject> | null, DBReducerActionType> Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
@ -653,6 +655,16 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
onHide(); onHide();
}; };
const redirectURL = (url: string) => {
/* TODO (lyndsiWilliams): This check and passing history
as a prop can be removed once SQL Lab is in the SPA */
if (!isEmpty(history)) {
history?.push(url);
} else {
window.location.href = url;
}
};
// Database import logic // Database import logic
const { const {
state: { state: {
@ -1345,23 +1357,21 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
const renderCTABtns = () => ( const renderCTABtns = () => (
<StyledBtns> <StyledBtns>
<Button <Button
// eslint-disable-next-line no-return-assign
buttonStyle="secondary" buttonStyle="secondary"
onClick={() => { onClick={() => {
setLoading(true); setLoading(true);
fetchAndSetDB(); fetchAndSetDB();
window.location.href = '/tablemodelview/list#create'; redirectURL('/dataset/add/');
}} }}
> >
{t('CREATE DATASET')} {t('CREATE DATASET')}
</Button> </Button>
<Button <Button
buttonStyle="secondary" buttonStyle="secondary"
// eslint-disable-next-line no-return-assign
onClick={() => { onClick={() => {
setLoading(true); setLoading(true);
fetchAndSetDB(); fetchAndSetDB();
window.location.href = `/superset/sqllab/?db=true`; redirectURL(`/superset/sqllab/?db=true`);
}} }}
> >
{t('QUERY DATA IN SQL LAB')} {t('QUERY DATA IN SQL LAB')}

View File

@ -20,6 +20,14 @@ import React from 'react';
import { render, screen } from 'spec/helpers/testing-library'; import { render, screen } from 'spec/helpers/testing-library';
import AddDataset from 'src/views/CRUD/data/dataset/AddDataset'; import AddDataset from 'src/views/CRUD/data/dataset/AddDataset';
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
describe('AddDataset', () => { describe('AddDataset', () => {
it('renders a blank state AddDataset', async () => { it('renders a blank state AddDataset', async () => {
render(<AddDataset />, { useRedux: true }); render(<AddDataset />, { useRedux: true });

View File

@ -40,6 +40,7 @@ export const exampleDataset: DatasetObject[] = [
id: 1, id: 1,
database_name: 'test_database', database_name: 'test_database',
owners: [1], owners: [1],
backend: 'test_backend',
}, },
schema: 'test_schema', schema: 'test_schema',
dataset_name: 'example_dataset', dataset_name: 'example_dataset',

View File

@ -17,8 +17,9 @@
* under the License. * under the License.
*/ */
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { SupersetClient } from '@superset-ui/core'; import { SupersetClient, logging, t } from '@superset-ui/core';
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types'; import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import DatasetPanel from './DatasetPanel'; import DatasetPanel from './DatasetPanel';
import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types'; import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
@ -94,9 +95,17 @@ const DatasetPanelWrapper = ({
setColumnList([]); setColumnList([]);
setHasColumns?.(false); setHasColumns?.(false);
setHasError(true); setHasError(true);
// eslint-disable-next-line no-console addDangerToast(
console.error( t(
`The API response from ${path} does not match the IDatabaseTable interface.`, 'The API response from %s does not match the IDatabaseTable interface.',
path,
),
);
logging.error(
t(
'The API response from %s does not match the IDatabaseTable interface.',
path,
),
); );
} }
} catch (error) { } catch (error) {

View File

@ -20,6 +20,14 @@ import React from 'react';
import { render, screen } from 'spec/helpers/testing-library'; import { render, screen } from 'spec/helpers/testing-library';
import Footer from 'src/views/CRUD/data/dataset/AddDataset/Footer'; import Footer from 'src/views/CRUD/data/dataset/AddDataset/Footer';
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
const mockedProps = { const mockedProps = {
url: 'realwebsite.com', url: 'realwebsite.com',
}; };

View File

@ -17,6 +17,7 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import { useHistory } from 'react-router-dom';
import Button from 'src/components/Button'; import Button from 'src/components/Button';
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import { useSingleViewResource } from 'src/views/CRUD/hooks'; import { useSingleViewResource } from 'src/views/CRUD/hooks';
@ -49,12 +50,12 @@ const LOG_ACTIONS = [
]; ];
function Footer({ function Footer({
url,
datasetObject, datasetObject,
addDangerToast, addDangerToast,
hasColumns = false, hasColumns = false,
datasets, datasets,
}: FooterProps) { }: FooterProps) {
const history = useHistory();
const { createResource } = useSingleViewResource<Partial<DatasetObject>>( const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
'dataset', 'dataset',
t('dataset'), t('dataset'),
@ -72,11 +73,6 @@ function Footer({
return LOG_ACTIONS[value]; return LOG_ACTIONS[value];
}; };
const goToPreviousUrl = () => {
// this is a placeholder url until the final feature gets implemented
// at that point we will be passing in the url of the previous location.
window.location.href = url;
};
const cancelButtonOnClick = () => { const cancelButtonOnClick = () => {
if (!datasetObject) { if (!datasetObject) {
@ -85,7 +81,7 @@ function Footer({
const logAction = createLogAction(datasetObject); const logAction = createLogAction(datasetObject);
logEvent(logAction, datasetObject); logEvent(logAction, datasetObject);
} }
goToPreviousUrl(); history.goBack();
}; };
const tooltipText = t('Select a database table.'); const tooltipText = t('Select a database table.');
@ -104,13 +100,13 @@ function Footer({
if (typeof response === 'number') { if (typeof response === 'number') {
logEvent(LOG_ACTIONS_DATASET_CREATION_SUCCESS, datasetObject); logEvent(LOG_ACTIONS_DATASET_CREATION_SUCCESS, datasetObject);
// When a dataset is created the response we get is its ID number // When a dataset is created the response we get is its ID number
goToPreviousUrl(); history.push(`/chart/add/?dataset=${datasetObject.table_name}`);
} }
}); });
} }
}; };
const CREATE_DATASET_TEXT = t('Create Dataset'); const CREATE_DATASET_TEXT = t('Create dataset and create chart');
const disabledCheck = const disabledCheck =
!datasetObject?.table_name || !datasetObject?.table_name ||
!hasColumns || !hasColumns ||

View File

@ -21,6 +21,7 @@ import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { render, screen, waitFor } from 'spec/helpers/testing-library'; import { render, screen, waitFor } from 'spec/helpers/testing-library';
import LeftPanel from 'src/views/CRUD/data/dataset/AddDataset/LeftPanel'; import LeftPanel from 'src/views/CRUD/data/dataset/AddDataset/LeftPanel';
import { exampleDataset } from 'src/views/CRUD/data/dataset/AddDataset/DatasetPanel/fixtures';
const databasesEndpoint = 'glob:*/api/v1/database/?q*'; const databasesEndpoint = 'glob:*/api/v1/database/?q*';
const schemasEndpoint = 'glob:*/api/v1/database/*/schemas*'; const schemasEndpoint = 'glob:*/api/v1/database/*/schemas*';
@ -136,8 +137,8 @@ fetchMock.get(schemasEndpoint, {
}); });
fetchMock.get(tablesEndpoint, { fetchMock.get(tablesEndpoint, {
count: 3, tableLength: 3,
result: [ options: [
{ value: 'Sheet1', type: 'table', extra: null }, { value: 'Sheet1', type: 'table', extra: null },
{ value: 'Sheet2', type: 'table', extra: null }, { value: 'Sheet2', type: 'table', extra: null },
{ value: 'Sheet3', type: 'table', extra: null }, { value: 'Sheet3', type: 'table', extra: null },
@ -181,7 +182,7 @@ test('does not render blank state if there is nothing selected', async () => {
}); });
test('renders list of options when user clicks on schema', async () => { test('renders list of options when user clicks on schema', async () => {
render(<LeftPanel setDataset={mockFun} schema="schema_a" dbId={1} />, { render(<LeftPanel setDataset={mockFun} dataset={exampleDataset[0]} />, {
useRedux: true, useRedux: true,
}); });
@ -189,23 +190,21 @@ test('renders list of options when user clicks on schema', async () => {
const databaseSelect = screen.getByRole('combobox', { const databaseSelect = screen.getByRole('combobox', {
name: 'Select database or type database name', name: 'Select database or type database name',
}); });
// Schema select should be disabled until database is selected userEvent.click(databaseSelect);
expect(await screen.findByText('test-postgres')).toBeInTheDocument();
userEvent.click(screen.getByText('test-postgres'));
// Schema select will be automatically populated if there is only one schema
const schemaSelect = screen.getByRole('combobox', { const schemaSelect = screen.getByRole('combobox', {
name: /select schema or type schema name/i, name: /select schema or type schema name/i,
}); });
userEvent.click(databaseSelect);
expect(await screen.findByText('test-postgres')).toBeInTheDocument();
expect(schemaSelect).toBeDisabled();
userEvent.click(screen.getByText('test-postgres'));
// Wait for schema field to be enabled
await waitFor(() => { await waitFor(() => {
expect(schemaSelect).toBeEnabled(); expect(schemaSelect).toBeEnabled();
}); });
}); });
test('searches for a table name', async () => { test('searches for a table name', async () => {
render(<LeftPanel setDataset={mockFun} schema="schema_a" dbId={1} />, { render(<LeftPanel setDataset={mockFun} dataset={exampleDataset[0]} />, {
useRedux: true, useRedux: true,
}); });
@ -245,9 +244,8 @@ test('renders a warning icon when a table name has a pre-existing dataset', asyn
render( render(
<LeftPanel <LeftPanel
setDataset={mockFun} setDataset={mockFun}
schema="schema_a" dataset={exampleDataset[0]}
dbId={1} datasetNames={['Sheet2']}
datasets={['Sheet2']}
/>, />,
{ {
useRedux: true, useRedux: true,

View File

@ -16,7 +16,13 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useEffect, useState, SetStateAction, Dispatch } from 'react'; import React, {
useEffect,
useState,
SetStateAction,
Dispatch,
useCallback,
} from 'react';
import rison from 'rison'; import rison from 'rison';
import { import {
SupersetClient, SupersetClient,
@ -41,13 +47,16 @@ import {
emptyStateComponent, emptyStateComponent,
} from 'src/components/EmptyState'; } from 'src/components/EmptyState';
import { useToasts } from 'src/components/MessageToasts/withToasts'; import { useToasts } from 'src/components/MessageToasts/withToasts';
import { DatasetActionType } from '../types'; import { LocalStorageKeys, getItem } from 'src/utils/localStorageHelpers';
import {
DatasetActionType,
DatasetObject,
} from 'src/views/CRUD/data/dataset/AddDataset/types';
interface LeftPanelProps { interface LeftPanelProps {
setDataset: Dispatch<SetStateAction<object>>; setDataset: Dispatch<SetStateAction<object>>;
schema?: string | null | undefined; dataset?: Partial<DatasetObject> | null;
dbId?: number; datasetNames?: (string | null | undefined)[] | undefined;
datasets?: (string | null | undefined)[] | undefined;
} }
const SearchIcon = styled(Icons.Search)` const SearchIcon = styled(Icons.Search)`
@ -146,9 +155,8 @@ const LeftPanelStyle = styled.div`
export default function LeftPanel({ export default function LeftPanel({
setDataset, setDataset,
schema, dataset,
dbId, datasetNames,
datasets,
}: LeftPanelProps) { }: LeftPanelProps) {
const theme = useTheme(); const theme = useTheme();
@ -161,11 +169,14 @@ export default function LeftPanel({
const { addDangerToast } = useToasts(); const { addDangerToast } = useToasts();
const setDatabase = (db: Partial<DatabaseObject>) => { const setDatabase = useCallback(
setDataset({ type: DatasetActionType.selectDatabase, payload: { db } }); (db: Partial<DatabaseObject>) => {
setSelectedTable(null); setDataset({ type: DatasetActionType.selectDatabase, payload: { db } });
setResetTables(true); setSelectedTable(null);
}; setResetTables(true);
},
[setDataset],
);
const setTable = (tableName: string, index: number) => { const setTable = (tableName: string, index: number) => {
setSelectedTable(index); setSelectedTable(index);
@ -175,28 +186,32 @@ export default function LeftPanel({
}); });
}; };
const getTablesList = (url: string) => { const getTablesList = useCallback(
SupersetClient.get({ url }) (url: string) => {
.then(({ json }) => { SupersetClient.get({ url })
const options: TableOption[] = json.result.map((table: Table) => { .then(({ json }) => {
const option: TableOption = { const options: TableOption[] = json.options.map((table: Table) => {
value: table.value, const option: TableOption = {
label: <TableOption table={table} />, value: table.value,
text: table.label, label: <TableOption table={table} />,
}; text: table.label,
};
return option; return option;
});
setTableOptions(options);
setLoadTables(false);
setResetTables(false);
setRefresh(false);
})
.catch(error => {
addDangerToast(t('There was an error fetching tables'));
logging.error(t('There was an error fetching tables'), error);
}); });
},
setTableOptions(options); [addDangerToast],
setLoadTables(false); );
setResetTables(false);
setRefresh(false);
})
.catch(error =>
logging.error('There was an error fetching tables', error),
);
};
const setSchema = (schema: string) => { const setSchema = (schema: string) => {
if (schema) { if (schema) {
@ -210,7 +225,19 @@ export default function LeftPanel({
setResetTables(true); setResetTables(true);
}; };
const encodedSchema = schema ? encodeURIComponent(schema) : undefined; const encodedSchema = dataset?.schema
? encodeURIComponent(dataset?.schema)
: undefined;
useEffect(() => {
const currentUserSelectedDb = getItem(
LocalStorageKeys.db,
null,
) as DatabaseObject;
if (currentUserSelectedDb) {
setDatabase(currentUserSelectedDb);
}
}, [setDatabase]);
useEffect(() => { useEffect(() => {
if (loadTables) { if (loadTables) {
@ -219,10 +246,10 @@ export default function LeftPanel({
schema_name: encodedSchema, schema_name: encodedSchema,
}); });
const endpoint = `/api/v1/database/${dbId}/tables/?q=${params}`; const endpoint = `/api/v1/database/${dataset?.db?.id}/tables/?q=${params}`;
getTablesList(endpoint); getTablesList(endpoint);
} }
}, [loadTables]); }, [loadTables, dataset?.db?.id, encodedSchema, getTablesList, refresh]);
useEffect(() => { useEffect(() => {
if (resetTables) { if (resetTables) {
@ -266,6 +293,7 @@ export default function LeftPanel({
{SELECT_DATABASE_AND_SCHEMA_TEXT} {SELECT_DATABASE_AND_SCHEMA_TEXT}
</p> </p>
<DatabaseSelector <DatabaseSelector
db={dataset?.db}
handleError={addDangerToast} handleError={addDangerToast}
onDbChange={setDatabase} onDbChange={setDatabase}
onSchemaChange={setSchema} onSchemaChange={setSchema}
@ -273,7 +301,7 @@ export default function LeftPanel({
onEmptyResults={onEmptyResults} onEmptyResults={onEmptyResults}
/> />
{loadTables && !refresh && Loader(TABLE_LOADING_TEXT)} {loadTables && !refresh && Loader(TABLE_LOADING_TEXT)}
{schema && !loadTables && !tableOptions.length && !searchVal && ( {dataset?.schema && !loadTables && !tableOptions.length && !searchVal && (
<div className="emptystate"> <div className="emptystate">
<EmptyStateMedium <EmptyStateMedium
image="empty-table.svg" image="empty-table.svg"
@ -283,7 +311,7 @@ export default function LeftPanel({
</div> </div>
)} )}
{schema && (tableOptions.length > 0 || searchVal.length > 0) && ( {dataset?.schema && (tableOptions.length > 0 || searchVal.length > 0) && (
<> <>
<Form> <Form>
<p className="table-title">{SELECT_DATABASE_TABLE_TEXT}</p> <p className="table-title">{SELECT_DATABASE_TABLE_TEXT}</p>
@ -327,7 +355,7 @@ export default function LeftPanel({
onClick={() => setTable(option.value, i)} onClick={() => setTable(option.value, i)}
> >
{option.label} {option.label}
{datasets?.includes(option.value) && ( {datasetNames?.includes(option.value) && (
<Icons.Warning <Icons.Warning
iconColor={ iconColor={
selectedTable === i selectedTable === i

View File

@ -16,10 +16,16 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useReducer, Reducer, useEffect, useState } from 'react'; import React, {
import { logging } from '@superset-ui/core'; useReducer,
Reducer,
useEffect,
useState,
useCallback,
} from 'react';
import { logging, t } from '@superset-ui/core';
import { UseGetDatasetsList } from 'src/views/CRUD/data/hooks'; import { UseGetDatasetsList } from 'src/views/CRUD/data/hooks';
import rison from 'rison'; import { addDangerToast } from 'src/components/MessageToasts/actions';
import Header from './Header'; import Header from './Header';
import DatasetPanel from './DatasetPanel'; import DatasetPanel from './DatasetPanel';
import LeftPanel from './LeftPanel'; import LeftPanel from './LeftPanel';
@ -82,30 +88,29 @@ export default function AddDataset() {
? encodeURIComponent(dataset?.schema) ? encodeURIComponent(dataset?.schema)
: undefined; : undefined;
const queryParams = dataset?.schema const getDatasetsList = useCallback(async () => {
? rison.encode_uri({ if (dataset?.schema) {
filters: [ const filters = [
{ col: 'schema', opr: 'eq', value: encodedSchema }, { col: 'database', opr: 'rel_o_m', value: dataset?.db?.id },
{ col: 'sql', opr: 'dataset_is_null_or_empty', value: '!t' }, { col: 'schema', opr: 'eq', value: encodedSchema },
], { col: 'sql', opr: 'dataset_is_null_or_empty', value: true },
}) ];
: undefined; await UseGetDatasetsList(filters)
.then(results => {
const getDatasetsList = async () => { setDatasets(results);
await UseGetDatasetsList(queryParams) })
.then(json => { .catch(error => {
setDatasets(json?.result); addDangerToast(t('There was an error fetching dataset'));
}) logging.error(t('There was an error fetching dataset'), error);
.catch(error => });
logging.error('There was an error fetching dataset', error), }
); }, [dataset?.db?.id, dataset?.schema, encodedSchema]);
};
useEffect(() => { useEffect(() => {
if (dataset?.schema) { if (dataset?.schema) {
getDatasetsList(); getDatasetsList();
} }
}, [dataset?.schema]); }, [dataset?.schema, getDatasetsList]);
const HeaderComponent = () => ( const HeaderComponent = () => (
<Header setDataset={setDataset} title={dataset?.table_name} /> <Header setDataset={setDataset} title={dataset?.table_name} />
@ -114,9 +119,8 @@ export default function AddDataset() {
const LeftPanelComponent = () => ( const LeftPanelComponent = () => (
<LeftPanel <LeftPanel
setDataset={setDataset} setDataset={setDataset}
schema={dataset?.schema} dataset={dataset}
dbId={dataset?.db?.id} datasetNames={datasetNames}
datasets={datasetNames}
/> />
); );

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { DatabaseObject } from 'src/components/DatabaseSelector';
export enum DatasetActionType { export enum DatasetActionType {
selectDatabase, selectDatabase,
selectSchema, selectSchema,
@ -24,11 +26,7 @@ export enum DatasetActionType {
} }
export interface DatasetObject { export interface DatasetObject {
db: { db: DatabaseObject & { owners: [number] };
id: number;
database_name?: string;
owners?: number[];
};
schema?: string | null; schema?: string | null;
dataset_name: string; dataset_name: string;
table_name?: string | null; table_name?: string | null;

View File

@ -1,172 +0,0 @@
/**
* 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, { FunctionComponent, useState, useEffect } from 'react';
import { styled, t } from '@superset-ui/core';
import { useSingleViewResource } from 'src/views/CRUD/hooks';
import Modal from 'src/components/Modal';
import TableSelector from 'src/components/TableSelector';
import withToasts from 'src/components/MessageToasts/withToasts';
import { DatabaseObject } from 'src/components/DatabaseSelector';
import {
getItem,
LocalStorageKeys,
setItem,
} from 'src/utils/localStorageHelpers';
import { isEmpty } from 'lodash';
type DatasetAddObject = {
id: number;
database: number;
schema: string;
table_name: string;
};
interface DatasetModalProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
onDatasetAdd?: (dataset: DatasetAddObject) => void;
onHide: () => void;
show: boolean;
history?: any; // So we can render the modal when not using SPA
}
const TableSelectorContainer = styled.div`
padding-bottom: 340px;
width: 65%;
`;
const DatasetModal: FunctionComponent<DatasetModalProps> = ({
addDangerToast,
onDatasetAdd,
onHide,
show,
history,
}) => {
const [currentDatabase, setCurrentDatabase] = useState<
DatabaseObject | undefined
>();
const [currentSchema, setSchema] = useState<string | undefined>('');
const [currentTableName, setTableName] = useState('');
const [disableSave, setDisableSave] = useState(true);
const {
createResource,
state: { loading },
} = useSingleViewResource<Partial<DatasetAddObject>>(
'dataset',
t('dataset'),
addDangerToast,
);
useEffect(() => {
setDisableSave(currentDatabase === undefined || currentTableName === '');
}, [currentTableName, currentDatabase]);
useEffect(() => {
const currentUserSelectedDb = getItem(
LocalStorageKeys.db,
null,
) as DatabaseObject;
if (currentUserSelectedDb) setCurrentDatabase(currentUserSelectedDb);
}, []);
const onDbChange = (db: DatabaseObject) => {
setCurrentDatabase(db);
};
const onSchemaChange = (schema?: string) => {
setSchema(schema);
};
const onTableChange = (tableName: string) => {
setTableName(tableName);
};
const clearModal = () => {
setSchema('');
setTableName('');
setCurrentDatabase(undefined);
setDisableSave(true);
};
const cleanup = () => {
clearModal();
setItem(LocalStorageKeys.db, null);
};
const hide = () => {
cleanup();
onHide();
};
const onSave = () => {
if (currentDatabase === undefined) {
return;
}
const data = {
database: currentDatabase.id,
...(currentSchema ? { schema: currentSchema } : {}),
table_name: currentTableName,
};
createResource(data).then(response => {
if (!response) {
return;
}
if (onDatasetAdd) {
onDatasetAdd({ id: response.id, ...response });
}
// We need to be able to work with no SPA routes opening the modal
// So useHistory wont be available always thus we check for it
if (!isEmpty(history)) {
history?.push(`/chart/add?dataset=${currentTableName}`);
cleanup();
} else {
window.location.href = `/chart/add?dataset=${currentTableName}`;
cleanup();
onHide();
}
});
};
return (
<Modal
disablePrimaryButton={disableSave}
primaryButtonLoading={loading}
onHandledPrimaryAction={onSave}
onHide={hide}
primaryButtonName={t('Add Dataset and Create Chart')}
show={show}
title={t('Add dataset')}
>
<TableSelectorContainer>
<TableSelector
clearable={false}
formMode
database={currentDatabase}
schema={currentSchema}
tableValue={currentTableName}
onDbChange={onDbChange}
onSchemaChange={onSchemaChange}
onTableSelectChange={onTableChange}
handleError={addDangerToast}
/>
</TableSelectorContainer>
</Modal>
);
};
export default withToasts(DatasetModal);

View File

@ -25,6 +25,14 @@ import DatasetPanel from 'src/views/CRUD/data/dataset/AddDataset/DatasetPanel';
import RightPanel from 'src/views/CRUD/data/dataset/AddDataset/RightPanel'; import RightPanel from 'src/views/CRUD/data/dataset/AddDataset/RightPanel';
import Footer from 'src/views/CRUD/data/dataset/AddDataset/Footer'; import Footer from 'src/views/CRUD/data/dataset/AddDataset/Footer';
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => ({
push: mockHistoryPush,
}),
}));
describe('DatasetLayout', () => { describe('DatasetLayout', () => {
it('renders nothing when no components are passed in', () => { it('renders nothing when no components are passed in', () => {
render(<DatasetLayout />); render(<DatasetLayout />);

View File

@ -22,16 +22,14 @@ import React, {
useState, useState,
useMemo, useMemo,
useCallback, useCallback,
useEffect,
} from 'react'; } from 'react';
import { useHistory } from 'react-router-dom';
import rison from 'rison'; import rison from 'rison';
import { useHistory, useLocation } from 'react-router-dom';
import { import {
createFetchRelated, createFetchRelated,
createFetchDistinct, createFetchDistinct,
createErrorHandler, createErrorHandler,
} from 'src/views/CRUD/utils'; } from 'src/views/CRUD/utils';
import { getItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
import { ColumnObject } from 'src/views/CRUD/data/dataset/types'; import { ColumnObject } from 'src/views/CRUD/data/dataset/types';
import { useListViewResource } from 'src/views/CRUD/hooks'; import { useListViewResource } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
@ -60,7 +58,6 @@ import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags';
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip'; import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
import { isUserAdmin } from 'src/dashboard/util/permissionUtils'; import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
import { GenericLink } from 'src/components/GenericLink/GenericLink'; import { GenericLink } from 'src/components/GenericLink/GenericLink';
import AddDatasetModal from './AddDatasetModal';
import { import {
PAGE_SIZE, PAGE_SIZE,
@ -139,6 +136,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
addSuccessToast, addSuccessToast,
user, user,
}) => { }) => {
const history = useHistory();
const { const {
state: { state: {
loading, loading,
@ -152,9 +150,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
refreshData, refreshData,
} = useListViewResource<Dataset>('dataset', t('dataset'), addDangerToast); } = useListViewResource<Dataset>('dataset', t('dataset'), addDangerToast);
const [datasetAddModalOpen, setDatasetAddModalOpen] =
useState<boolean>(false);
const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState< const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
(Dataset & { chart_count: number; dashboard_count: number }) | null (Dataset & { chart_count: number; dashboard_count: number }) | null
>(null); >(null);
@ -191,12 +186,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); hasPerm('can_export') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT);
const initialSort = SORT_BY; const initialSort = SORT_BY;
useEffect(() => {
const db = getItem(LocalStorageKeys.db, null);
if (!loading && db) {
setDatasetAddModalOpen(true);
}
}, [loading]);
const openDatasetEditModal = useCallback( const openDatasetEditModal = useCallback(
({ id }: Dataset) => { ({ id }: Dataset) => {
@ -603,26 +592,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
}); });
} }
const CREATE_HASH = '#create';
const location = useLocation();
const history = useHistory();
// Sync Dataset Add modal with #create hash
useEffect(() => {
const modalOpen = location.hash === CREATE_HASH && canCreate;
setDatasetAddModalOpen(modalOpen);
}, [canCreate, location.hash]);
// Add #create hash
const openDatasetAddModal = useCallback(() => {
history.replace(`${location.pathname}${location.search}${CREATE_HASH}`);
}, [history, location.pathname, location.search]);
// Remove #create hash
const closeDatasetAddModal = useCallback(() => {
history.replace(`${location.pathname}${location.search}`);
}, [history, location.pathname, location.search]);
if (canCreate) { if (canCreate) {
buttonArr.push({ buttonArr.push({
name: ( name: (
@ -630,7 +599,9 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
<i className="fa fa-plus" /> {t('Dataset')}{' '} <i className="fa fa-plus" /> {t('Dataset')}{' '}
</> </>
), ),
onClick: openDatasetAddModal, onClick: () => {
history.push('/dataset/add/');
},
buttonStyle: 'primary', buttonStyle: 'primary',
}); });
@ -727,12 +698,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
return ( return (
<> <>
<SubMenu {...menuData} /> <SubMenu {...menuData} />
<AddDatasetModal
show={datasetAddModalOpen}
onHide={closeDatasetAddModal}
onDatasetAdd={refreshData}
history={history}
/>
{datasetCurrentlyDeleting && ( {datasetCurrentlyDeleting && (
<DeleteModal <DeleteModal
description={t( description={t(

View File

@ -62,6 +62,7 @@ export const PanelRow = styled(Row)`
export const FooterRow = styled(Row)` export const FooterRow = styled(Row)`
flex: 0 0 auto; flex: 0 0 auto;
height: ${({ theme }) => theme.gridUnit * 16}px; height: ${({ theme }) => theme.gridUnit * 16}px;
z-index: 0;
`; `;
export const StyledLayoutHeader = styled.div` export const StyledLayoutHeader = styled.div`

View File

@ -17,7 +17,10 @@
* under the License. * under the License.
*/ */
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { SupersetClient, logging } from '@superset-ui/core'; import { SupersetClient, logging, t } from '@superset-ui/core';
import { addDangerToast } from 'src/components/MessageToasts/actions';
import { DatasetObject } from 'src/views/CRUD/data/dataset/AddDataset/types';
import rison from 'rison';
type BaseQueryObject = { type BaseQueryObject = {
id: number; id: number;
@ -75,11 +78,38 @@ export function useQueryPreviewState<D extends BaseQueryObject = any>({
}; };
} }
export const UseGetDatasetsList = (queryParams: string | undefined) => /**
SupersetClient.get({ * Retrieves all pages of dataset results
endpoint: `/api/v1/dataset/?q=${queryParams}`, */
}) export const UseGetDatasetsList = async (filters: object[]) => {
.then(({ json }) => json) let results: DatasetObject[] = [];
.catch(error => let page = 0;
logging.error('There was an error fetching dataset', error), let count;
);
// If count is undefined or less than results, we need to
// asynchronously retrieve a page of dataset results
while (count === undefined || results.length < count) {
const queryParams = rison.encode_uri({ filters, page });
try {
// eslint-disable-next-line no-await-in-loop
const response = await SupersetClient.get({
endpoint: `/api/v1/dataset/?q=${queryParams}`,
});
// Reassign local count to response's count
({ count } = response.json);
const {
json: { result },
} = response;
results = [...results, ...result];
page += 1;
} catch (error) {
addDangerToast(t('There was an error fetching dataset'));
logging.error(t('There was an error fetching dataset'), error);
}
}
return results;
};

View File

@ -30,9 +30,6 @@ jest.mock('react-redux', () => ({
})); }));
jest.mock('src/views/CRUD/data/database/DatabaseModal', () => () => <span />); jest.mock('src/views/CRUD/data/database/DatabaseModal', () => () => <span />);
jest.mock('src/views/CRUD/data/dataset/AddDatasetModal.tsx', () => () => (
<span />
));
const dropdownItems = [ const dropdownItems = [
{ {

View File

@ -41,6 +41,7 @@ import { isUserAdmin } from 'src/dashboard/util/permissionUtils';
import { import {
MenuObjectProps, MenuObjectProps,
UserWithPermissionsAndRoles, UserWithPermissionsAndRoles,
MenuObjectChildProps,
} from 'src/types/bootstrapTypes'; } from 'src/types/bootstrapTypes';
import { RootState } from 'src/dashboard/types'; import { RootState } from 'src/dashboard/types';
import LanguagePicker from './LanguagePicker'; import LanguagePicker from './LanguagePicker';
@ -51,7 +52,6 @@ import {
GlobalMenuDataOptions, GlobalMenuDataOptions,
RightMenuProps, RightMenuProps,
} from './types'; } from './types';
import AddDatasetModal from '../CRUD/data/dataset/AddDatasetModal';
const extensionsRegistry = getExtensionsRegistry(); const extensionsRegistry = getExtensionsRegistry();
@ -143,7 +143,6 @@ const RightMenu = ({
HAS_GSHEETS_INSTALLED, HAS_GSHEETS_INSTALLED,
} = useSelector<any, ExtentionConfigs>(state => state.common.conf); } = useSelector<any, ExtentionConfigs>(state => state.common.conf);
const [showDatabaseModal, setShowDatabaseModal] = useState<boolean>(false); const [showDatabaseModal, setShowDatabaseModal] = useState<boolean>(false);
const [showDatasetModal, setShowDatasetModal] = useState<boolean>(false);
const [engine, setEngine] = useState<string>(''); const [engine, setEngine] = useState<string>('');
const canSql = findPermission('can_sqllab', 'Superset', roles); const canSql = findPermission('can_sqllab', 'Superset', roles);
const canDashboard = findPermission('can_write', 'Dashboard', roles); const canDashboard = findPermission('can_write', 'Dashboard', roles);
@ -179,6 +178,7 @@ const RightMenu = ({
{ {
label: t('Create dataset'), label: t('Create dataset'),
name: GlobalMenuDataOptions.DATASET_CREATION, name: GlobalMenuDataOptions.DATASET_CREATION,
url: '/dataset/add/',
perm: canDataset && nonExamplesDBConnected, perm: canDataset && nonExamplesDBConnected,
}, },
{ {
@ -191,18 +191,21 @@ const RightMenu = ({
name: 'Upload a CSV', name: 'Upload a CSV',
url: '/csvtodatabaseview/form', url: '/csvtodatabaseview/form',
perm: canUploadCSV && showUploads, perm: canUploadCSV && showUploads,
disable: isAdmin && !allowUploads,
}, },
{ {
label: t('Upload columnar file to database'), label: t('Upload columnar file to database'),
name: 'Upload a Columnar file', name: 'Upload a Columnar file',
url: '/columnartodatabaseview/form', url: '/columnartodatabaseview/form',
perm: canUploadColumnar && showUploads, perm: canUploadColumnar && showUploads,
disable: isAdmin && !allowUploads,
}, },
{ {
label: t('Upload Excel file to database'), label: t('Upload Excel file to database'),
name: 'Upload Excel', name: 'Upload Excel',
url: '/exceltodatabaseview/form', url: '/exceltodatabaseview/form',
perm: canUploadExcel && showUploads, perm: canUploadExcel && showUploads,
disable: isAdmin && !allowUploads,
}, },
], ],
}, },
@ -286,8 +289,6 @@ const RightMenu = ({
} else if (itemChose.key === GlobalMenuDataOptions.GOOGLE_SHEETS) { } else if (itemChose.key === GlobalMenuDataOptions.GOOGLE_SHEETS) {
setShowDatabaseModal(true); setShowDatabaseModal(true);
setEngine('Google Sheets'); setEngine('Google Sheets');
} else if (itemChose.key === GlobalMenuDataOptions.DATASET_CREATION) {
setShowDatasetModal(true);
} }
}; };
@ -296,19 +297,12 @@ const RightMenu = ({
setShowDatabaseModal(false); setShowDatabaseModal(false);
}; };
const handleOnHideDatasetModalModal = () => {
setShowDatasetModal(false);
};
const isDisabled = isAdmin && !allowUploads;
const tooltipText = t( const tooltipText = t(
"Enable 'Allow file uploads to database' in any database's settings", "Enable 'Allow file uploads to database' in any database's settings",
); );
const buildMenuItem = (item: Record<string, any>) => { const buildMenuItem = (item: MenuObjectChildProps) =>
const disabledText = isDisabled && item.url; item.disable ? (
return disabledText ? (
<Menu.Item key={item.name} css={styledDisabled}> <Menu.Item key={item.name} css={styledDisabled}>
<Tooltip placement="top" title={tooltipText}> <Tooltip placement="top" title={tooltipText}>
{item.label} {item.label}
@ -319,7 +313,6 @@ const RightMenu = ({
{item.url ? <a href={item.url}> {item.label} </a> : item.label} {item.url ? <a href={item.url}> {item.label} </a> : item.label}
</Menu.Item> </Menu.Item>
); );
};
const onMenuOpen = (openKeys: string[]) => { const onMenuOpen = (openKeys: string[]) => {
// We should query the API only if opening Data submenus // We should query the API only if opening Data submenus
@ -344,7 +337,6 @@ const RightMenu = ({
); );
const handleDatabaseAdd = () => setQuery({ databaseAdded: true }); const handleDatabaseAdd = () => setQuery({ databaseAdded: true });
const handleDatasetAdd = () => setQuery({ datasetAdded: true });
const theme = useTheme(); const theme = useTheme();
@ -358,13 +350,6 @@ const RightMenu = ({
onDatabaseAdd={handleDatabaseAdd} onDatabaseAdd={handleDatabaseAdd}
/> />
)} )}
{canDataset && (
<AddDatasetModal
onHide={handleOnHideDatasetModalModal}
show={showDatasetModal}
onDatasetAdd={handleDatasetAdd}
/>
)}
{environmentTag?.text && ( {environmentTag?.text && (
<Label <Label
css={{ borderRadius: `${theme.gridUnit * 125}px` }} css={{ borderRadius: `${theme.gridUnit * 125}px` }}

View File

@ -228,10 +228,7 @@ class DatasetEditor(BaseSupersetView):
@has_access @has_access
@permission_name("read") @permission_name("read")
def root(self) -> FlaskResponse: def root(self) -> FlaskResponse:
dev = request.args.get("testing") return super().render_app_template()
if dev is not None:
return super().render_app_template()
return redirect("/")
@expose("/<pk>", methods=["GET"]) @expose("/<pk>", methods=["GET"])
@has_access @has_access