mirror of
https://github.com/apache/superset.git
synced 2026-04-18 23:55:00 +00:00
Implement a React-based table editor (#5186)
* A React table editor * addressing comments * Fix SelectAsyncControl error on clear * fix tests * more corrections * Removed <strong>
This commit is contained in:
committed by
GitHub
parent
aa14bac5c7
commit
68ba63fcd9
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import CollectionTable from '../../../src/CRUD/CollectionTable';
|
||||
import mockDatasource from '../../fixtures/mockDatasource';
|
||||
|
||||
const props = {
|
||||
collection: mockDatasource['7__table'].columns,
|
||||
tableColumns: ['column_name', 'type', 'groupby'],
|
||||
};
|
||||
|
||||
describe('CollectionTable', () => {
|
||||
|
||||
let wrapper;
|
||||
let el;
|
||||
|
||||
beforeEach(() => {
|
||||
el = <CollectionTable {...props} />;
|
||||
wrapper = shallow(el);
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(el)).to.equal(true);
|
||||
});
|
||||
|
||||
it('renders a table', () => {
|
||||
const length = mockDatasource['7__table'].columns.length;
|
||||
expect(wrapper.find('table')).to.have.lengthOf(1);
|
||||
expect(wrapper.find('tbody tr.row')).to.have.lengthOf(length);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import DashboardBuilder from '../../../../src/dashboard/containers/DashboardBuil
|
||||
|
||||
// mock data
|
||||
import chartQueries, { sliceId as chartId } from '../fixtures/mockChartQueries';
|
||||
import datasources from '../fixtures/mockDatasource';
|
||||
import datasources from '../../../fixtures/mockDatasource';
|
||||
import dashboardInfo from '../fixtures/mockDashboardInfo';
|
||||
import { dashboardLayout } from '../fixtures/mockDashboardLayout';
|
||||
import dashboardState from '../fixtures/mockDashboardState';
|
||||
|
||||
@@ -8,7 +8,7 @@ import Chart from '../../../../../src/dashboard/components/gridComponents/Chart'
|
||||
import SliceHeader from '../../../../../src/dashboard/components/SliceHeader';
|
||||
import ChartContainer from '../../../../../src/chart/ChartContainer';
|
||||
|
||||
import mockDatasource from '../../fixtures/mockDatasource';
|
||||
import mockDatasource from '../../../../fixtures/mockDatasource';
|
||||
import {
|
||||
sliceEntitiesForChart as sliceEntities,
|
||||
sliceId,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { datasourceId } from './mockDatasource';
|
||||
import { datasourceId } from '../../../fixtures/mockDatasource';
|
||||
|
||||
export const sliceId = 18;
|
||||
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
export const id = 7;
|
||||
export const datasourceId = `${id}__table`;
|
||||
|
||||
export default {
|
||||
[datasourceId]: {
|
||||
verbose_map: {
|
||||
count: 'COUNT(*)',
|
||||
__timestamp: 'Time',
|
||||
sum__sum_girls: 'sum__sum_girls',
|
||||
name: 'name',
|
||||
avg__sum_girls: 'avg__sum_girls',
|
||||
gender: 'gender',
|
||||
sum_girls: 'sum_girls',
|
||||
ds: 'ds',
|
||||
sum__sum_boys: 'sum__sum_boys',
|
||||
state: 'state',
|
||||
num: 'num',
|
||||
sum__num: 'sum__num',
|
||||
sum_boys: 'sum_boys',
|
||||
avg__num: 'avg__num',
|
||||
avg__sum_boys: 'avg__sum_boys',
|
||||
},
|
||||
gb_cols: [['gender', 'gender'], ['name', 'name'], ['state', 'state']],
|
||||
metrics: [
|
||||
{
|
||||
expression: 'SUM(birth_names.num)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__num',
|
||||
metric_name: 'sum__num',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'AVG(birth_names.num)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__num',
|
||||
metric_name: 'avg__num',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'SUM(birth_names.sum_boys)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__sum_boys',
|
||||
metric_name: 'sum__sum_boys',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'AVG(birth_names.sum_boys)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__sum_boys',
|
||||
metric_name: 'avg__sum_boys',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'SUM(birth_names.sum_girls)',
|
||||
warning_text: null,
|
||||
verbose_name: 'sum__sum_girls',
|
||||
metric_name: 'sum__sum_girls',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'AVG(birth_names.sum_girls)',
|
||||
warning_text: null,
|
||||
verbose_name: 'avg__sum_girls',
|
||||
metric_name: 'avg__sum_girls',
|
||||
description: null,
|
||||
},
|
||||
{
|
||||
expression: 'COUNT(*)',
|
||||
warning_text: null,
|
||||
verbose_name: 'COUNT(*)',
|
||||
metric_name: 'count',
|
||||
description: null,
|
||||
},
|
||||
],
|
||||
column_formats: {},
|
||||
columns: [
|
||||
{
|
||||
type: 'DATETIME',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: true,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'ds',
|
||||
},
|
||||
{
|
||||
type: 'VARCHAR(16)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'gender',
|
||||
},
|
||||
{
|
||||
type: 'VARCHAR(255)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'num',
|
||||
},
|
||||
{
|
||||
type: 'VARCHAR(10)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
column_name: 'state',
|
||||
},
|
||||
{
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'sum_boys',
|
||||
},
|
||||
{
|
||||
type: 'BIGINT',
|
||||
description: null,
|
||||
filterable: false,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: false,
|
||||
column_name: 'sum_girls',
|
||||
},
|
||||
],
|
||||
id,
|
||||
granularity_sqla: [['ds', 'ds']],
|
||||
name: 'birth_names',
|
||||
database: {
|
||||
allow_multi_schema_metadata_fetch: null,
|
||||
name: 'main',
|
||||
backend: 'sqlite',
|
||||
},
|
||||
time_grain_sqla: [
|
||||
[null, 'Time Column'],
|
||||
['PT1H', 'hour'],
|
||||
['P1D', 'day'],
|
||||
['P1W', 'week'],
|
||||
['P1M', 'month'],
|
||||
],
|
||||
filterable_cols: [
|
||||
['gender', 'gender'],
|
||||
['name', 'name'],
|
||||
['state', 'state'],
|
||||
],
|
||||
all_cols: [
|
||||
['ds', 'ds'],
|
||||
['gender', 'gender'],
|
||||
['name', 'name'],
|
||||
['num', 'num'],
|
||||
['state', 'state'],
|
||||
['sum_boys', 'sum_boys'],
|
||||
['sum_girls', 'sum_girls'],
|
||||
],
|
||||
filter_select: true,
|
||||
order_by_choices: [
|
||||
['["ds", true]', 'ds [asc]'],
|
||||
['["ds", false]', 'ds [desc]'],
|
||||
['["gender", true]', 'gender [asc]'],
|
||||
['["gender", false]', 'gender [desc]'],
|
||||
['["name", true]', 'name [asc]'],
|
||||
['["name", false]', 'name [desc]'],
|
||||
['["num", true]', 'num [asc]'],
|
||||
['["num", false]', 'num [desc]'],
|
||||
['["state", true]', 'state [asc]'],
|
||||
['["state", false]', 'state [desc]'],
|
||||
['["sum_boys", true]', 'sum_boys [asc]'],
|
||||
['["sum_boys", false]', 'sum_boys [desc]'],
|
||||
['["sum_girls", true]', 'sum_girls [asc]'],
|
||||
['["sum_girls", false]', 'sum_girls [desc]'],
|
||||
],
|
||||
metrics_combo: [
|
||||
['count', 'COUNT(*)'],
|
||||
['avg__num', 'avg__num'],
|
||||
['avg__sum_boys', 'avg__sum_boys'],
|
||||
['avg__sum_girls', 'avg__sum_girls'],
|
||||
['sum__num', 'sum__num'],
|
||||
['sum__sum_boys', 'sum__sum_boys'],
|
||||
['sum__sum_girls', 'sum__sum_girls'],
|
||||
],
|
||||
type: 'table',
|
||||
edit_url: '/tablemodelview/edit/7',
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sliceId as id } from './mockChartQueries';
|
||||
import { datasourceId } from './mockDatasource';
|
||||
import { datasourceId } from '../../../fixtures/mockDatasource';
|
||||
|
||||
export const sliceId = id;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { dashboardLayout } from './mockDashboardLayout';
|
||||
import dashboardInfo from './mockDashboardInfo';
|
||||
import dashboardState from './mockDashboardState';
|
||||
import messageToasts from '../../messageToasts/mockMessageToasts';
|
||||
import datasources from './mockDatasource';
|
||||
import datasources from '../../../fixtures/mockDatasource';
|
||||
import sliceEntities from './mockSliceEntities';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { Tabs } from 'react-bootstrap';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import $ from 'jquery';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import DatasourceEditor from '../../../src/datasource/DatasourceEditor';
|
||||
import mockDatasource from '../../fixtures/mockDatasource';
|
||||
|
||||
const props = {
|
||||
datasource: mockDatasource['7__table'],
|
||||
addSuccessToast: () => {},
|
||||
addDangerToast: () => {},
|
||||
onChange: sinon.spy(),
|
||||
};
|
||||
const extraColumn = {
|
||||
column_name: 'new_column',
|
||||
type: 'VARCHAR(10)',
|
||||
description: null,
|
||||
filterable: true,
|
||||
verbose_name: null,
|
||||
is_dttm: false,
|
||||
expression: '',
|
||||
groupby: true,
|
||||
};
|
||||
|
||||
describe('DatasourceEditor', () => {
|
||||
const mockStore = configureStore([]);
|
||||
const store = mockStore({});
|
||||
|
||||
let wrapper;
|
||||
let el;
|
||||
let ajaxStub;
|
||||
let inst;
|
||||
|
||||
beforeEach(() => {
|
||||
ajaxStub = sinon.stub($, 'ajax');
|
||||
el = <DatasourceEditor {...props} />;
|
||||
wrapper = shallow(el, { context: { store } }).dive();
|
||||
inst = wrapper.instance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ajaxStub.restore();
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(el)).to.equal(true);
|
||||
});
|
||||
|
||||
it('renders Tabs', () => {
|
||||
expect(wrapper.find(Tabs)).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('makes an async request', () => {
|
||||
wrapper.setState({ activeTabKey: 2 });
|
||||
const syncButton = wrapper.find('.sync-from-source');
|
||||
expect(syncButton).to.have.lengthOf(1);
|
||||
syncButton.simulate('click');
|
||||
expect(ajaxStub.calledOnce).to.equal(true);
|
||||
});
|
||||
|
||||
it('merges columns', () => {
|
||||
const numCols = props.datasource.columns.length;
|
||||
expect(inst.state.databaseColumns.length).to.equal(numCols);
|
||||
inst.mergeColumns([extraColumn]);
|
||||
expect(inst.state.databaseColumns.length).to.equal(numCols + 1);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { shallow } from 'enzyme';
|
||||
import $ from 'jquery';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import DatasourceModal from '../../../src/datasource/DatasourceModal';
|
||||
import DatasourceEditor from '../../../src/datasource/DatasourceEditor';
|
||||
import mockDatasource from '../../fixtures/mockDatasource';
|
||||
|
||||
const props = {
|
||||
datasource: mockDatasource['7__table'],
|
||||
addSuccessToast: () => {},
|
||||
addDangerToast: () => {},
|
||||
onChange: sinon.spy(),
|
||||
show: true,
|
||||
onHide: () => {},
|
||||
};
|
||||
|
||||
describe('DatasourceModal', () => {
|
||||
const mockStore = configureStore([]);
|
||||
const store = mockStore({});
|
||||
|
||||
let wrapper;
|
||||
let el;
|
||||
let ajaxStub;
|
||||
let inst;
|
||||
|
||||
beforeEach(() => {
|
||||
ajaxStub = sinon.stub($, 'ajax');
|
||||
el = <DatasourceModal {...props} />;
|
||||
wrapper = shallow(el, { context: { store } }).dive();
|
||||
inst = wrapper.instance();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ajaxStub.restore();
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(el)).to.equal(true);
|
||||
});
|
||||
|
||||
it('renders a Modal', () => {
|
||||
expect(wrapper.find(Modal)).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('renders a DatasourceEditor', () => {
|
||||
expect(wrapper.find(DatasourceEditor)).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('saves on confirm', () => {
|
||||
inst.onConfirmSave();
|
||||
expect(ajaxStub.calledOnce).to.equal(true);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import configureStore from 'redux-mock-store';
|
||||
import { expect } from 'chai';
|
||||
import { describe, it } from 'mocha';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Modal } from 'react-bootstrap';
|
||||
import DatasourceModal from '../../../../src/datasource/DatasourceModal';
|
||||
import DatasourceControl from '../../../../src/explore/components/controls/DatasourceControl';
|
||||
|
||||
const defaultProps = {
|
||||
@@ -35,6 +35,6 @@ describe('DatasourceControl', () => {
|
||||
|
||||
it('renders a Modal', () => {
|
||||
const wrapper = setup();
|
||||
expect(wrapper.find(Modal)).to.have.lengthOf(1);
|
||||
expect(wrapper.find(DatasourceModal)).to.have.lengthOf(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
/* eslint-disable no-unused-expressions */
|
||||
import { it, describe } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import $ from 'jquery';
|
||||
import * as chartActions from '../../../src/chart/chartAction';
|
||||
import * as actions from '../../../src/explore/actions/exploreActions';
|
||||
import { defaultState } from '../../../src/explore/store';
|
||||
import exploreReducer from '../../../src/explore/reducers/exploreReducer';
|
||||
import * as actions from '../../../src/explore/actions/exploreActions';
|
||||
|
||||
describe('reducers', () => {
|
||||
it('sets correct control value given a key and value', () => {
|
||||
@@ -20,65 +17,3 @@ describe('reducers', () => {
|
||||
expect(newState.controls.show_legend.value).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetching actions', () => {
|
||||
let dispatch;
|
||||
let request;
|
||||
let ajaxStub;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = sinon.spy();
|
||||
ajaxStub = sinon.stub($, 'ajax');
|
||||
});
|
||||
afterEach(() => {
|
||||
ajaxStub.restore();
|
||||
});
|
||||
|
||||
describe('fetchDatasourceMetadata', () => {
|
||||
const datasourceKey = '1__table';
|
||||
|
||||
const makeRequest = (alsoTriggerQuery = false) => {
|
||||
request = actions.fetchDatasourceMetadata(datasourceKey, alsoTriggerQuery);
|
||||
request(dispatch);
|
||||
};
|
||||
|
||||
it('calls fetchDatasourceStarted', () => {
|
||||
makeRequest();
|
||||
expect(dispatch.args[0][0].type).to.equal(actions.FETCH_DATASOURCE_STARTED);
|
||||
});
|
||||
|
||||
it('makes the ajax request', () => {
|
||||
makeRequest();
|
||||
expect(ajaxStub.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('calls correct url', () => {
|
||||
const url = `/superset/fetch_datasource_metadata?datasourceKey=${datasourceKey}`;
|
||||
makeRequest();
|
||||
expect(ajaxStub.getCall(0).args[0].url).to.equal(url);
|
||||
});
|
||||
|
||||
it('calls correct actions on error', () => {
|
||||
ajaxStub.yieldsTo('error', { responseJSON: { error: 'error text' } });
|
||||
makeRequest();
|
||||
expect(dispatch.callCount).to.equal(2);
|
||||
expect(dispatch.getCall(1).args[0].type).to.equal(actions.FETCH_DATASOURCE_FAILED);
|
||||
});
|
||||
|
||||
it('calls correct actions on success', () => {
|
||||
ajaxStub.yieldsTo('success', { data: '' });
|
||||
makeRequest();
|
||||
expect(dispatch.callCount).to.equal(4);
|
||||
expect(dispatch.getCall(1).args[0].type).to.equal(actions.SET_DATASOURCE);
|
||||
expect(dispatch.getCall(2).args[0].type).to.equal(actions.FETCH_DATASOURCE_SUCCEEDED);
|
||||
expect(dispatch.getCall(3).args[0].type).to.equal(actions.RESET_FIELDS);
|
||||
});
|
||||
|
||||
it('triggers query if flag is set', () => {
|
||||
ajaxStub.yieldsTo('success', { data: '' });
|
||||
makeRequest(true);
|
||||
expect(dispatch.callCount).to.equal(5);
|
||||
expect(dispatch.getCall(4).args[0].type).to.equal(chartActions.TRIGGER_QUERY);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { describe, it } from 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { user } from './fixtures';
|
||||
import CreatedContent from '../../../src/profile/components/CreatedContent';
|
||||
import TableLoader from '../../../src/profile/components/TableLoader';
|
||||
import TableLoader from '../../../src/components/TableLoader';
|
||||
|
||||
|
||||
describe('CreatedContent', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { expect } from 'chai';
|
||||
|
||||
import { user } from './fixtures';
|
||||
import Favorites from '../../../src/profile/components/Favorites';
|
||||
import TableLoader from '../../../src/profile/components/TableLoader';
|
||||
import TableLoader from '../../../src/components/TableLoader';
|
||||
|
||||
describe('Favorites', () => {
|
||||
const mockedProps = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { expect } from 'chai';
|
||||
|
||||
import { user } from './fixtures';
|
||||
import RecentActivity from '../../../src/profile/components/RecentActivity';
|
||||
import TableLoader from '../../../src/profile/components/TableLoader';
|
||||
import TableLoader from '../../../src/components/TableLoader';
|
||||
|
||||
|
||||
describe('RecentActivity', () => {
|
||||
|
||||
@@ -9,10 +9,6 @@ import { table, defaultQueryEditor, databases, tables } from './fixtures';
|
||||
import SqlEditorLeftBar from '../../../src/SqlLab/components/SqlEditorLeftBar';
|
||||
import TableElement from '../../../src/SqlLab/components/TableElement';
|
||||
|
||||
global.notify = {
|
||||
error: () => {},
|
||||
};
|
||||
|
||||
describe('SqlEditorLeftBar', () => {
|
||||
const mockedProps = {
|
||||
actions: {
|
||||
|
||||
Reference in New Issue
Block a user