mirror of
https://github.com/apache/superset.git
synced 2024-09-12 00:29:39 -04:00
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();
|
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,
|
||||||
|
@ -231,7 +231,9 @@ const ColumnSelectPopover = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setDatasetAndClose = () => {
|
const setDatasetAndClose = () => {
|
||||||
if (setDatasetModal) setDatasetModal(true);
|
if (setDatasetModal) {
|
||||||
|
setDatasetModal(true);
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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')} `}
|
||||||
|
@ -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);
|
||||||
},
|
},
|
||||||
|
@ -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',
|
||||||
|
@ -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')}
|
||||||
|
@ -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 });
|
||||||
|
@ -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',
|
||||||
|
@ -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) {
|
||||||
|
@ -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',
|
||||||
};
|
};
|
||||||
|
@ -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 ||
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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 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 />);
|
||||||
|
@ -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(
|
||||||
|
@ -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`
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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 = [
|
||||||
{
|
{
|
||||||
|
@ -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` }}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user