mirror of https://github.com/apache/superset.git
parent
39c96d0568
commit
577ac81686
|
@ -17,9 +17,9 @@
|
|||
[metadata]
|
||||
name = Superset
|
||||
summary = a data exploration platform
|
||||
description-file = README.md
|
||||
description_file = README.md
|
||||
author = Apache Superset Dev
|
||||
author-email = dev@superset.apache.org
|
||||
author_email = dev@superset.apache.org
|
||||
license = Apache License, Version 2.0
|
||||
|
||||
[files]
|
||||
|
|
|
@ -186,6 +186,27 @@ 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,9 +231,7 @@ const ColumnSelectPopover = ({
|
|||
}, []);
|
||||
|
||||
const setDatasetAndClose = () => {
|
||||
if (setDatasetModal) {
|
||||
setDatasetModal(true);
|
||||
}
|
||||
if (setDatasetModal) setDatasetModal(true);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
|
|
@ -337,7 +337,10 @@ export class ChartCreation extends React.PureComponent<
|
|||
const isButtonDisabled = this.isBtnDisabled();
|
||||
const datasetHelpText = this.state.canCreateDataset ? (
|
||||
<span data-test="dataset-write">
|
||||
<Link to="/dataset/add/" data-test="add-chart-new-dataset">
|
||||
<Link
|
||||
to="/tablemodelview/list/#create"
|
||||
data-test="add-chart-new-dataset"
|
||||
>
|
||||
{t('Add a dataset')}
|
||||
</Link>
|
||||
{` ${t('or')} `}
|
||||
|
|
|
@ -21,7 +21,6 @@ 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';
|
||||
|
@ -158,9 +157,6 @@ 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);
|
||||
},
|
||||
|
|
|
@ -43,14 +43,6 @@ jest.mock('@superset-ui/core', () => ({
|
|||
isFeatureEnabled: () => true,
|
||||
}));
|
||||
|
||||
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',
|
||||
|
|
|
@ -31,7 +31,6 @@ import React, {
|
|||
useReducer,
|
||||
Reducer,
|
||||
} from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { setItem, LocalStorageKeys } from 'src/utils/localStorageHelpers';
|
||||
import { UploadChangeParam, UploadFile } from 'antd/lib/upload/interface';
|
||||
import Tabs from 'src/components/Tabs';
|
||||
|
@ -519,7 +518,6 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
t('database'),
|
||||
addDangerToast,
|
||||
);
|
||||
const history = useHistory();
|
||||
|
||||
const [tabKey, setTabKey] = useState<string>(DEFAULT_TAB_KEY);
|
||||
const [availableDbs, getAvailableDbs] = useAvailableDatabases();
|
||||
|
@ -1297,7 +1295,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
onClick={() => {
|
||||
setLoading(true);
|
||||
fetchAndSetDB();
|
||||
history.push('/dataset/add/');
|
||||
window.location.href = '/tablemodelview/list#create';
|
||||
}}
|
||||
>
|
||||
{t('CREATE DATASET')}
|
||||
|
@ -1308,7 +1306,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
|
|||
onClick={() => {
|
||||
setLoading(true);
|
||||
fetchAndSetDB();
|
||||
history.push(`/superset/sqllab/?db=true`);
|
||||
window.location.href = `/superset/sqllab/?db=true`;
|
||||
}}
|
||||
>
|
||||
{t('QUERY DATA IN SQL LAB')}
|
||||
|
|
|
@ -20,14 +20,6 @@ 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,7 +40,6 @@ export const exampleDataset: DatasetObject[] = [
|
|||
id: 1,
|
||||
database_name: 'test_database',
|
||||
owners: [1],
|
||||
backend: 'test_backend',
|
||||
},
|
||||
schema: 'test_schema',
|
||||
dataset_name: 'example_dataset',
|
||||
|
|
|
@ -17,9 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { SupersetClient, logging, t } from '@superset-ui/core';
|
||||
import { SupersetClient } 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';
|
||||
|
||||
|
@ -95,17 +94,9 @@ const DatasetPanelWrapper = ({
|
|||
setColumnList([]);
|
||||
setHasColumns?.(false);
|
||||
setHasError(true);
|
||||
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,
|
||||
),
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
`The API response from ${path} does not match the IDatabaseTable interface.`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -20,14 +20,6 @@ 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,7 +17,6 @@
|
|||
* 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';
|
||||
|
@ -50,12 +49,12 @@ const LOG_ACTIONS = [
|
|||
];
|
||||
|
||||
function Footer({
|
||||
url,
|
||||
datasetObject,
|
||||
addDangerToast,
|
||||
hasColumns = false,
|
||||
datasets,
|
||||
}: FooterProps) {
|
||||
const history = useHistory();
|
||||
const { createResource } = useSingleViewResource<Partial<DatasetObject>>(
|
||||
'dataset',
|
||||
t('dataset'),
|
||||
|
@ -73,6 +72,11 @@ 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) {
|
||||
|
@ -81,7 +85,7 @@ function Footer({
|
|||
const logAction = createLogAction(datasetObject);
|
||||
logEvent(logAction, datasetObject);
|
||||
}
|
||||
history.goBack();
|
||||
goToPreviousUrl();
|
||||
};
|
||||
|
||||
const tooltipText = t('Select a database table.');
|
||||
|
@ -100,13 +104,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
|
||||
history.push(`/chart/add/?dataset=${datasetObject.table_name}`);
|
||||
goToPreviousUrl();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const CREATE_DATASET_TEXT = t('Create Dataset and Create Chart');
|
||||
const CREATE_DATASET_TEXT = t('Create Dataset');
|
||||
const disabledCheck =
|
||||
!datasetObject?.table_name ||
|
||||
!hasColumns ||
|
||||
|
|
|
@ -21,7 +21,6 @@ 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*';
|
||||
|
@ -182,7 +181,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} dataset={exampleDataset[0]} />, {
|
||||
render(<LeftPanel setDataset={mockFun} schema="schema_a" dbId={1} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
|
@ -190,21 +189,23 @@ test('renders list of options when user clicks on schema', async () => {
|
|||
const databaseSelect = screen.getByRole('combobox', {
|
||||
name: 'Select database or type database name',
|
||||
});
|
||||
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
|
||||
// Schema select should be disabled until database is selected
|
||||
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} dataset={exampleDataset[0]} />, {
|
||||
render(<LeftPanel setDataset={mockFun} schema="schema_a" dbId={1} />, {
|
||||
useRedux: true,
|
||||
});
|
||||
|
||||
|
@ -244,8 +245,9 @@ test('renders a warning icon when a table name has a pre-existing dataset', asyn
|
|||
render(
|
||||
<LeftPanel
|
||||
setDataset={mockFun}
|
||||
dataset={exampleDataset[0]}
|
||||
datasetNames={['Sheet2']}
|
||||
schema="schema_a"
|
||||
dbId={1}
|
||||
datasets={['Sheet2']}
|
||||
/>,
|
||||
{
|
||||
useRedux: true,
|
||||
|
|
|
@ -16,13 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
SetStateAction,
|
||||
Dispatch,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import React, { useEffect, useState, SetStateAction, Dispatch } from 'react';
|
||||
import {
|
||||
SupersetClient,
|
||||
t,
|
||||
|
@ -46,16 +40,13 @@ import {
|
|||
emptyStateComponent,
|
||||
} from 'src/components/EmptyState';
|
||||
import { useToasts } from 'src/components/MessageToasts/withToasts';
|
||||
import { LocalStorageKeys, getItem } from 'src/utils/localStorageHelpers';
|
||||
import {
|
||||
DatasetActionType,
|
||||
DatasetObject,
|
||||
} from 'src/views/CRUD/data/dataset/AddDataset/types';
|
||||
import { DatasetActionType } from '../types';
|
||||
|
||||
interface LeftPanelProps {
|
||||
setDataset: Dispatch<SetStateAction<object>>;
|
||||
dataset?: Partial<DatasetObject> | null;
|
||||
datasetNames?: (string | null | undefined)[] | undefined;
|
||||
schema?: string | null | undefined;
|
||||
dbId?: number;
|
||||
datasets?: (string | null | undefined)[] | undefined;
|
||||
}
|
||||
|
||||
const SearchIcon = styled(Icons.Search)`
|
||||
|
@ -154,8 +145,9 @@ const LeftPanelStyle = styled.div`
|
|||
|
||||
export default function LeftPanel({
|
||||
setDataset,
|
||||
dataset,
|
||||
datasetNames,
|
||||
schema,
|
||||
dbId,
|
||||
datasets,
|
||||
}: LeftPanelProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
|
@ -168,14 +160,11 @@ export default function LeftPanel({
|
|||
|
||||
const { addDangerToast } = useToasts();
|
||||
|
||||
const setDatabase = useCallback(
|
||||
(db: Partial<DatabaseObject>) => {
|
||||
setDataset({ type: DatasetActionType.selectDatabase, payload: { db } });
|
||||
setSelectedTable(null);
|
||||
setResetTables(true);
|
||||
},
|
||||
[setDataset],
|
||||
);
|
||||
const setDatabase = (db: Partial<DatabaseObject>) => {
|
||||
setDataset({ type: DatasetActionType.selectDatabase, payload: { db } });
|
||||
setSelectedTable(null);
|
||||
setResetTables(true);
|
||||
};
|
||||
|
||||
const setTable = (tableName: string, index: number) => {
|
||||
setSelectedTable(index);
|
||||
|
@ -185,32 +174,28 @@ export default function LeftPanel({
|
|||
});
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
const getTablesList = (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;
|
||||
});
|
||||
|
||||
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);
|
||||
return option;
|
||||
});
|
||||
},
|
||||
[addDangerToast],
|
||||
);
|
||||
|
||||
setTableOptions(options);
|
||||
setLoadTables(false);
|
||||
setResetTables(false);
|
||||
setRefresh(false);
|
||||
})
|
||||
.catch(error =>
|
||||
logging.error('There was an error fetching tables', error),
|
||||
);
|
||||
};
|
||||
|
||||
const setSchema = (schema: string) => {
|
||||
if (schema) {
|
||||
|
@ -224,28 +209,16 @@ export default function LeftPanel({
|
|||
setResetTables(true);
|
||||
};
|
||||
|
||||
const encodedSchema = dataset?.schema
|
||||
? encodeURIComponent(dataset?.schema)
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const currentUserSelectedDb = getItem(
|
||||
LocalStorageKeys.db,
|
||||
null,
|
||||
) as DatabaseObject;
|
||||
if (currentUserSelectedDb) {
|
||||
setDatabase(currentUserSelectedDb);
|
||||
}
|
||||
}, [setDatabase]);
|
||||
const encodedSchema = schema ? encodeURIComponent(schema) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (loadTables) {
|
||||
const endpoint = encodeURI(
|
||||
`/superset/tables/${dataset?.db?.id}/${encodedSchema}/${refresh}/`,
|
||||
`/superset/tables/${dbId}/${encodedSchema}/${refresh}/`,
|
||||
);
|
||||
getTablesList(endpoint);
|
||||
}
|
||||
}, [loadTables, dataset?.db?.id, encodedSchema, getTablesList, refresh]);
|
||||
}, [loadTables]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resetTables) {
|
||||
|
@ -289,7 +262,6 @@ export default function LeftPanel({
|
|||
{SELECT_DATABASE_AND_SCHEMA_TEXT}
|
||||
</p>
|
||||
<DatabaseSelector
|
||||
db={dataset?.db}
|
||||
handleError={addDangerToast}
|
||||
onDbChange={setDatabase}
|
||||
onSchemaChange={setSchema}
|
||||
|
@ -297,7 +269,7 @@ export default function LeftPanel({
|
|||
onEmptyResults={onEmptyResults}
|
||||
/>
|
||||
{loadTables && !refresh && Loader(TABLE_LOADING_TEXT)}
|
||||
{dataset?.schema && !loadTables && !tableOptions.length && !searchVal && (
|
||||
{schema && !loadTables && !tableOptions.length && !searchVal && (
|
||||
<div className="emptystate">
|
||||
<EmptyStateMedium
|
||||
image="empty-table.svg"
|
||||
|
@ -307,7 +279,7 @@ export default function LeftPanel({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{dataset?.schema && (tableOptions.length > 0 || searchVal.length > 0) && (
|
||||
{schema && (tableOptions.length > 0 || searchVal.length > 0) && (
|
||||
<>
|
||||
<Form>
|
||||
<p className="table-title">{SELECT_DATABASE_TABLE_TEXT}</p>
|
||||
|
@ -351,7 +323,7 @@ export default function LeftPanel({
|
|||
onClick={() => setTable(option.value, i)}
|
||||
>
|
||||
{option.label}
|
||||
{datasetNames?.includes(option.value) && (
|
||||
{datasets?.includes(option.value) && (
|
||||
<Icons.Warning
|
||||
iconColor={
|
||||
selectedTable === i
|
||||
|
|
|
@ -16,17 +16,10 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, {
|
||||
useReducer,
|
||||
Reducer,
|
||||
useEffect,
|
||||
useState,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { logging, t } from '@superset-ui/core';
|
||||
import React, { useReducer, Reducer, useEffect, useState } from 'react';
|
||||
import { logging } from '@superset-ui/core';
|
||||
import { UseGetDatasetsList } from 'src/views/CRUD/data/hooks';
|
||||
import rison from 'rison';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import Header from './Header';
|
||||
import DatasetPanel from './DatasetPanel';
|
||||
import LeftPanel from './LeftPanel';
|
||||
|
@ -92,29 +85,27 @@ export default function AddDataset() {
|
|||
const queryParams = dataset?.schema
|
||||
? rison.encode_uri({
|
||||
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: '!t' },
|
||||
],
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const getDatasetsList = useCallback(async () => {
|
||||
const getDatasetsList = async () => {
|
||||
await UseGetDatasetsList(queryParams)
|
||||
.then(json => {
|
||||
setDatasets(json?.result);
|
||||
})
|
||||
.catch(error => {
|
||||
addDangerToast(t('There was an error fetching dataset'));
|
||||
logging.error(t('There was an error fetching dataset'), error);
|
||||
});
|
||||
}, [queryParams]);
|
||||
.catch(error =>
|
||||
logging.error('There was an error fetching dataset', error),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dataset?.schema) {
|
||||
getDatasetsList();
|
||||
}
|
||||
}, [dataset?.schema, getDatasetsList]);
|
||||
}, [dataset?.schema]);
|
||||
|
||||
const HeaderComponent = () => (
|
||||
<Header setDataset={setDataset} title={dataset?.table_name} />
|
||||
|
@ -123,8 +114,9 @@ export default function AddDataset() {
|
|||
const LeftPanelComponent = () => (
|
||||
<LeftPanel
|
||||
setDataset={setDataset}
|
||||
dataset={dataset}
|
||||
datasetNames={datasetNames}
|
||||
schema={dataset?.schema}
|
||||
dbId={dataset?.db?.id}
|
||||
datasets={datasetNames}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { DatabaseObject } from 'src/components/DatabaseSelector';
|
||||
|
||||
export enum DatasetActionType {
|
||||
selectDatabase,
|
||||
selectSchema,
|
||||
|
@ -26,7 +24,11 @@ export enum DatasetActionType {
|
|||
}
|
||||
|
||||
export interface DatasetObject {
|
||||
db: DatabaseObject & { owners: [number] };
|
||||
db: {
|
||||
id: number;
|
||||
database_name?: string;
|
||||
owners?: number[];
|
||||
};
|
||||
schema?: string | null;
|
||||
dataset_name: string;
|
||||
table_name?: string | null;
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
* 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,14 +25,6 @@ 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,14 +22,16 @@ 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';
|
||||
|
@ -58,6 +60,7 @@ 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,
|
||||
|
@ -136,7 +139,6 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
addSuccessToast,
|
||||
user,
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
const {
|
||||
state: {
|
||||
loading,
|
||||
|
@ -150,6 +152,9 @@ 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);
|
||||
|
@ -186,6 +191,12 @@ 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) => {
|
||||
|
@ -592,6 +603,26 @@ 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: (
|
||||
|
@ -599,9 +630,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
<i className="fa fa-plus" /> {t('Dataset')}{' '}
|
||||
</>
|
||||
),
|
||||
onClick: () => {
|
||||
history.push('/dataset/add/');
|
||||
},
|
||||
onClick: openDatasetAddModal,
|
||||
buttonStyle: 'primary',
|
||||
});
|
||||
|
||||
|
@ -698,6 +727,12 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
return (
|
||||
<>
|
||||
<SubMenu {...menuData} />
|
||||
<AddDatasetModal
|
||||
show={datasetAddModalOpen}
|
||||
onHide={closeDatasetAddModal}
|
||||
onDatasetAdd={refreshData}
|
||||
history={history}
|
||||
/>
|
||||
{datasetCurrentlyDeleting && (
|
||||
<DeleteModal
|
||||
description={t(
|
||||
|
|
|
@ -17,8 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { SupersetClient, logging, t } from '@superset-ui/core';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import { SupersetClient, logging } from '@superset-ui/core';
|
||||
|
||||
type BaseQueryObject = {
|
||||
id: number;
|
||||
|
@ -81,7 +80,6 @@ export const UseGetDatasetsList = (queryParams: string | undefined) =>
|
|||
endpoint: `/api/v1/dataset/?q=${queryParams}`,
|
||||
})
|
||||
.then(({ json }) => json)
|
||||
.catch(error => {
|
||||
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),
|
||||
);
|
||||
|
|
|
@ -30,6 +30,9 @@ 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 = [
|
||||
{
|
||||
|
|
|
@ -51,6 +51,7 @@ import {
|
|||
GlobalMenuDataOptions,
|
||||
RightMenuProps,
|
||||
} from './types';
|
||||
import AddDatasetModal from '../CRUD/data/dataset/AddDatasetModal';
|
||||
|
||||
const extensionsRegistry = getExtensionsRegistry();
|
||||
|
||||
|
@ -142,6 +143,7 @@ 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);
|
||||
|
@ -177,7 +179,6 @@ const RightMenu = ({
|
|||
{
|
||||
label: t('Create dataset'),
|
||||
name: GlobalMenuDataOptions.DATASET_CREATION,
|
||||
url: '/dataset/add/',
|
||||
perm: canDataset && nonExamplesDBConnected,
|
||||
},
|
||||
{
|
||||
|
@ -285,6 +286,8 @@ const RightMenu = ({
|
|||
} else if (itemChose.key === GlobalMenuDataOptions.GOOGLE_SHEETS) {
|
||||
setShowDatabaseModal(true);
|
||||
setEngine('Google Sheets');
|
||||
} else if (itemChose.key === GlobalMenuDataOptions.DATASET_CREATION) {
|
||||
setShowDatasetModal(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -293,6 +296,10 @@ const RightMenu = ({
|
|||
setShowDatabaseModal(false);
|
||||
};
|
||||
|
||||
const handleOnHideDatasetModalModal = () => {
|
||||
setShowDatasetModal(false);
|
||||
};
|
||||
|
||||
const isDisabled = isAdmin && !allowUploads;
|
||||
|
||||
const tooltipText = t(
|
||||
|
@ -337,6 +344,7 @@ const RightMenu = ({
|
|||
);
|
||||
|
||||
const handleDatabaseAdd = () => setQuery({ databaseAdded: true });
|
||||
const handleDatasetAdd = () => setQuery({ datasetAdded: true });
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
|
@ -350,6 +358,13 @@ const RightMenu = ({
|
|||
onDatabaseAdd={handleDatabaseAdd}
|
||||
/>
|
||||
)}
|
||||
{canDataset && (
|
||||
<AddDatasetModal
|
||||
onHide={handleOnHideDatasetModalModal}
|
||||
show={showDatasetModal}
|
||||
onDatasetAdd={handleDatasetAdd}
|
||||
/>
|
||||
)}
|
||||
{environmentTag?.text && (
|
||||
<Label
|
||||
css={{ borderRadius: `${theme.gridUnit * 125}px` }}
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
@ -206,7 +208,7 @@ class HiveEngineSpec(PrestoEngineSpec):
|
|||
with cls.get_engine(database) as engine:
|
||||
engine.execute(f"DROP TABLE IF EXISTS {str(table)}")
|
||||
|
||||
def _get_hive_type(dtype: np.dtype) -> str:
|
||||
def _get_hive_type(dtype: np.dtype[Any]) -> str:
|
||||
hive_type_by_dtype = {
|
||||
np.dtype("bool"): "BOOLEAN",
|
||||
np.dtype("float64"): "DOUBLE",
|
||||
|
|
|
@ -14,11 +14,13 @@
|
|||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from operator import eq, ge, gt, le, lt, ne
|
||||
from timeit import default_timer
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
@ -84,12 +86,12 @@ class AlertCommand(BaseCommand):
|
|||
except (KeyError, json.JSONDecodeError) as ex:
|
||||
raise AlertValidatorConfigError() from ex
|
||||
|
||||
def _validate_not_null(self, rows: np.recarray) -> None:
|
||||
def _validate_not_null(self, rows: np.recarray[Any, Any]) -> None:
|
||||
self._validate_result(rows)
|
||||
self._result = rows[0][1]
|
||||
|
||||
@staticmethod
|
||||
def _validate_result(rows: np.recarray) -> None:
|
||||
def _validate_result(rows: np.recarray[Any, Any]) -> None:
|
||||
# check if query return more than one row
|
||||
if len(rows) > 1:
|
||||
raise AlertQueryMultipleRowsError(
|
||||
|
@ -108,7 +110,7 @@ class AlertCommand(BaseCommand):
|
|||
)
|
||||
)
|
||||
|
||||
def _validate_operator(self, rows: np.recarray) -> None:
|
||||
def _validate_operator(self, rows: np.recarray[Any, Any]) -> None:
|
||||
self._validate_result(rows)
|
||||
if rows[0][1] in (0, None, np.nan):
|
||||
self._result = 0.0
|
||||
|
|
|
@ -24,6 +24,7 @@ from typing import Any, Dict, List, Optional, Tuple, Type
|
|||
import numpy as np
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
from numpy.typing import NDArray
|
||||
|
||||
from superset.db_engine_specs import BaseEngineSpec
|
||||
from superset.superset_typing import DbapiDescription, DbapiResult, ResultSetColumnType
|
||||
|
@ -62,16 +63,16 @@ def stringify(obj: Any) -> str:
|
|||
return json.dumps(obj, default=utils.json_iso_dttm_ser)
|
||||
|
||||
|
||||
def stringify_values(array: np.ndarray) -> np.ndarray:
|
||||
def stringify_values(array: NDArray[Any]) -> NDArray[Any]:
|
||||
result = np.copy(array)
|
||||
|
||||
with np.nditer(result, flags=["refs_ok"], op_flags=["readwrite"]) as it:
|
||||
with np.nditer(result, flags=["refs_ok"], op_flags=[["readwrite"]]) as it:
|
||||
for obj in it:
|
||||
if pd.isna(obj):
|
||||
if na_obj := pd.isna(obj):
|
||||
# pandas <NA> type cannot be converted to string
|
||||
obj[pd.isna(obj)] = None
|
||||
obj[na_obj] = None # type: ignore
|
||||
else:
|
||||
obj[...] = stringify(obj)
|
||||
obj[...] = stringify(obj) # type: ignore
|
||||
|
||||
return result
|
||||
|
||||
|
@ -106,7 +107,7 @@ class SupersetResultSet:
|
|||
pa_data: List[pa.Array] = []
|
||||
deduped_cursor_desc: List[Tuple[Any, ...]] = []
|
||||
numpy_dtype: List[Tuple[str, ...]] = []
|
||||
stringified_arr: np.ndarray
|
||||
stringified_arr: NDArray[Any]
|
||||
|
||||
if cursor_description:
|
||||
# get deduped list of column names
|
||||
|
@ -208,7 +209,7 @@ class SupersetResultSet:
|
|||
return table.to_pandas(integer_object_nulls=True, timestamp_as_object=True)
|
||||
|
||||
@staticmethod
|
||||
def first_nonempty(items: List[Any]) -> Any:
|
||||
def first_nonempty(items: NDArray[Any]) -> Any:
|
||||
return next((i for i in items if i), None)
|
||||
|
||||
def is_temporal(self, db_type_str: Optional[str]) -> bool:
|
||||
|
|
|
@ -57,10 +57,10 @@ def boxplot(
|
|||
"""
|
||||
|
||||
def quartile1(series: Series) -> float:
|
||||
return np.nanpercentile(series, 25, interpolation="midpoint")
|
||||
return np.nanpercentile(series, 25, interpolation="midpoint") # type: ignore
|
||||
|
||||
def quartile3(series: Series) -> float:
|
||||
return np.nanpercentile(series, 75, interpolation="midpoint")
|
||||
return np.nanpercentile(series, 75, interpolation="midpoint") # type: ignore
|
||||
|
||||
if whisker_type == PostProcessingBoxplotWhiskerType.TUKEY:
|
||||
|
||||
|
@ -99,8 +99,8 @@ def boxplot(
|
|||
return np.nanpercentile(series, low)
|
||||
|
||||
else:
|
||||
whisker_high = np.max
|
||||
whisker_low = np.min
|
||||
whisker_high = np.max # type: ignore
|
||||
whisker_low = np.min # type: ignore
|
||||
|
||||
def outliers(series: Series) -> Set[float]:
|
||||
above = series[series > whisker_high(series)]
|
||||
|
@ -126,7 +126,7 @@ def boxplot(
|
|||
# nanpercentile needs numeric values, otherwise the isnan function
|
||||
# that's used in the underlying function will fail
|
||||
for column in metrics:
|
||||
if df.dtypes[column] == np.object:
|
||||
if df.dtypes[column] == np.object_:
|
||||
df[column] = to_numeric(df[column], errors="coerce")
|
||||
|
||||
return aggregate(df, groupby=groupby, aggregates=aggregates)
|
||||
|
|
|
@ -85,7 +85,7 @@ def flatten(
|
|||
_columns = []
|
||||
for series in df.columns.to_flat_index():
|
||||
_cells = []
|
||||
for cell in series if is_sequence(series) else [series]:
|
||||
for cell in series if is_sequence(series) else [series]: # type: ignore
|
||||
if pd.notnull(cell):
|
||||
# every cell should be converted to string and escape comma
|
||||
_cells.append(escape_separator(str(cell)))
|
||||
|
|
|
@ -24,7 +24,7 @@ from pandas import DataFrame, NamedAgg
|
|||
|
||||
from superset.exceptions import InvalidPostProcessingError
|
||||
|
||||
NUMPY_FUNCTIONS = {
|
||||
NUMPY_FUNCTIONS: Dict[str, Callable[..., Any]] = {
|
||||
"average": np.average,
|
||||
"argmin": np.argmin,
|
||||
"argmax": np.argmax,
|
||||
|
|
|
@ -228,7 +228,10 @@ class DatasetEditor(BaseSupersetView):
|
|||
@has_access
|
||||
@permission_name("read")
|
||||
def root(self) -> FlaskResponse:
|
||||
return super().render_app_template()
|
||||
dev = request.args.get("testing")
|
||||
if dev is not None:
|
||||
return super().render_app_template()
|
||||
return redirect("/")
|
||||
|
||||
@expose("/<pk>", methods=["GET"])
|
||||
@has_access
|
||||
|
|
Loading…
Reference in New Issue