mirror of https://github.com/apache/superset.git
feat: Enable new dataset creation flow II (#22835)
This commit is contained in:
parent
ebed50fd12
commit
260ac40b23
|
@ -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,
|
||||
|
|
|
@ -231,7 +231,9 @@ const ColumnSelectPopover = ({
|
|||
}, []);
|
||||
|
||||
const setDatasetAndClose = () => {
|
||||
if (setDatasetModal) setDatasetModal(true);
|
||||
if (setDatasetModal) {
|
||||
setDatasetModal(true);
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
|
@ -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')} `}
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
|
@ -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 />);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
@ -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` }}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue