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();
});
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 () => {
const newProps = {
...props,

View File

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

View File

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

View File

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

View File

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

View File

@ -137,6 +137,7 @@ interface DatabaseModalProps {
show: boolean;
databaseId: number | undefined; // If included, will go into edit mode
dbEngine: string | undefined; // if included goto step 2 with engine already set
history?: any;
}
export enum ActionType {
@ -521,6 +522,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
show,
databaseId,
dbEngine,
history,
}) => {
const [db, setDB] = useReducer<
Reducer<Partial<DatabaseObject> | null, DBReducerActionType>
@ -653,6 +655,16 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
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
const {
state: {
@ -1345,23 +1357,21 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
const renderCTABtns = () => (
<StyledBtns>
<Button
// eslint-disable-next-line no-return-assign
buttonStyle="secondary"
onClick={() => {
setLoading(true);
fetchAndSetDB();
window.location.href = '/tablemodelview/list#create';
redirectURL('/dataset/add/');
}}
>
{t('CREATE DATASET')}
</Button>
<Button
buttonStyle="secondary"
// eslint-disable-next-line no-return-assign
onClick={() => {
setLoading(true);
fetchAndSetDB();
window.location.href = `/superset/sqllab/?db=true`;
redirectURL(`/superset/sqllab/?db=true`);
}}
>
{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 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', () => {
it('renders a blank state AddDataset', async () => {
render(<AddDataset />, { useRedux: true });

View File

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

View File

@ -17,8 +17,9 @@
* under the License.
*/
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 { addDangerToast } from 'src/components/MessageToasts/actions';
import DatasetPanel from './DatasetPanel';
import { ITableColumn, IDatabaseTable, isIDatabaseTable } from './types';
@ -94,9 +95,17 @@ const DatasetPanelWrapper = ({
setColumnList([]);
setHasColumns?.(false);
setHasError(true);
// eslint-disable-next-line no-console
console.error(
`The API response from ${path} does not match the IDatabaseTable interface.`,
addDangerToast(
t(
'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) {

View File

@ -20,6 +20,14 @@ import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
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 = {
url: 'realwebsite.com',
};

View File

@ -17,6 +17,7 @@
* under the License.
*/
import React from 'react';
import { useHistory } from 'react-router-dom';
import Button from 'src/components/Button';
import { t } from '@superset-ui/core';
import { useSingleViewResource } from 'src/views/CRUD/hooks';
@ -49,12 +50,12 @@ const LOG_ACTIONS = [
];
function Footer({
url,
datasetObject,
addDangerToast,
hasColumns = false,
datasets,
}: FooterProps) {
const history = useHistory();
const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
'dataset',
t('dataset'),
@ -72,11 +73,6 @@ function Footer({
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 = () => {
if (!datasetObject) {
@ -85,7 +81,7 @@ function Footer({
const logAction = createLogAction(datasetObject);
logEvent(logAction, datasetObject);
}
goToPreviousUrl();
history.goBack();
};
const tooltipText = t('Select a database table.');
@ -104,13 +100,13 @@ function Footer({
if (typeof response === 'number') {
logEvent(LOG_ACTIONS_DATASET_CREATION_SUCCESS, datasetObject);
// 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 =
!datasetObject?.table_name ||
!hasColumns ||

View File

@ -21,6 +21,7 @@ import fetchMock from 'fetch-mock';
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
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 schemasEndpoint = 'glob:*/api/v1/database/*/schemas*';
@ -136,8 +137,8 @@ fetchMock.get(schemasEndpoint, {
});
fetchMock.get(tablesEndpoint, {
count: 3,
result: [
tableLength: 3,
options: [
{ value: 'Sheet1', type: 'table', extra: null },
{ value: 'Sheet2', 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 () => {
render(<LeftPanel setDataset={mockFun} schema="schema_a" dbId={1} />, {
render(<LeftPanel setDataset={mockFun} dataset={exampleDataset[0]} />, {
useRedux: true,
});
@ -189,23 +190,21 @@ test('renders list of options when user clicks on schema', async () => {
const databaseSelect = screen.getByRole('combobox', {
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', {
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(() => {
expect(schemaSelect).toBeEnabled();
});
});
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,
});
@ -245,9 +244,8 @@ test('renders a warning icon when a table name has a pre-existing dataset', asyn
render(
<LeftPanel
setDataset={mockFun}
schema="schema_a"
dbId={1}
datasets={['Sheet2']}
dataset={exampleDataset[0]}
datasetNames={['Sheet2']}
/>,
{
useRedux: true,

View File

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

View File

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

View File

@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import { DatabaseObject } from 'src/components/DatabaseSelector';
export enum DatasetActionType {
selectDatabase,
selectSchema,
@ -24,11 +26,7 @@ export enum DatasetActionType {
}
export interface DatasetObject {
db: {
id: number;
database_name?: string;
owners?: number[];
};
db: DatabaseObject & { owners: [number] };
schema?: string | null;
dataset_name: string;
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 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', () => {
it('renders nothing when no components are passed in', () => {
render(<DatasetLayout />);

View File

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

View File

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

View File

@ -17,7 +17,10 @@
* under the License.
*/
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 = {
id: number;
@ -75,11 +78,38 @@ export function useQueryPreviewState<D extends BaseQueryObject = any>({
};
}
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),
);
/**
* Retrieves all pages of dataset results
*/
export const UseGetDatasetsList = async (filters: object[]) => {
let results: DatasetObject[] = [];
let page = 0;
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/dataset/AddDatasetModal.tsx', () => () => (
<span />
));
const dropdownItems = [
{

View File

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

View File

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