mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
refactor: table selector on dataset editor (#10914)
Co-authored-by: Maxime Beauchemin <maximebeauchemin@gmail.com>
This commit is contained in:
@@ -48,6 +48,7 @@ describe('Datasource control', () => {
|
||||
});
|
||||
|
||||
// create new metric
|
||||
cy.get('a[role="tab"]').contains('Metrics').click();
|
||||
cy.get('button').contains('Add Item', { timeout: 10000 }).click();
|
||||
cy.get('input[value="<new metric>"]').click();
|
||||
cy.get('input[value="<new metric>"]')
|
||||
@@ -64,6 +65,7 @@ describe('Datasource control', () => {
|
||||
// delete metric
|
||||
cy.get('#datasource_menu').click();
|
||||
cy.get('a').contains('Edit Datasource').click();
|
||||
cy.get('a[role="tab"]').contains('Metrics').click();
|
||||
cy.get(`input[value="${newMetricName}"]`)
|
||||
.closest('tr')
|
||||
.find('.fa-trash')
|
||||
|
||||
@@ -18,267 +18,273 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { shallow } from 'enzyme';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import sinon from 'sinon';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import thunk from 'redux-thunk';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
|
||||
import DatabaseSelector from 'src/components/DatabaseSelector';
|
||||
import TableSelector from 'src/components/TableSelector';
|
||||
import { initialState, tables } from '../sqllab/fixtures';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore(initialState);
|
||||
|
||||
const FETCH_SCHEMAS_ENDPOINT = 'glob:*/api/v1/database/*/schemas/*';
|
||||
const GET_TABLE_ENDPOINT = 'glob:*/superset/tables/1/*/*';
|
||||
const GET_TABLE_NAMES_ENDPOINT = 'glob:*/superset/tables/1/main/*';
|
||||
|
||||
const mockedProps = {
|
||||
clearable: false,
|
||||
database: { id: 1, database_name: 'main' },
|
||||
dbId: 1,
|
||||
formMode: false,
|
||||
getDbList: sinon.stub(),
|
||||
handleError: sinon.stub(),
|
||||
horizontal: false,
|
||||
onChange: sinon.stub(),
|
||||
onDbChange: sinon.stub(),
|
||||
onSchemaChange: sinon.stub(),
|
||||
onTableChange: sinon.stub(),
|
||||
sqlLabMode: true,
|
||||
tableName: '',
|
||||
tableNameSticky: true,
|
||||
};
|
||||
|
||||
const schemaOptions = {
|
||||
result: ['main', 'erf', 'superset'],
|
||||
};
|
||||
const selectedSchema = { label: 'main', title: 'main', value: 'main' };
|
||||
const selectedTable = {
|
||||
label: 'birth_names',
|
||||
schema: 'main',
|
||||
title: 'birth_names',
|
||||
value: 'birth_names',
|
||||
type: undefined,
|
||||
};
|
||||
|
||||
async function mountAndWait(props = mockedProps) {
|
||||
const mounted = mount(<TableSelector {...props} />, {
|
||||
context: { store },
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
});
|
||||
await waitForComponentToPaint(mounted);
|
||||
|
||||
return mounted;
|
||||
}
|
||||
|
||||
describe('TableSelector', () => {
|
||||
let mockedProps;
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
const store = mockStore(initialState);
|
||||
let wrapper;
|
||||
let inst;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedProps = {
|
||||
dbId: 1,
|
||||
schema: 'main',
|
||||
onSchemaChange: sinon.stub(),
|
||||
onDbChange: sinon.stub(),
|
||||
getDbList: sinon.stub(),
|
||||
onTableChange: sinon.stub(),
|
||||
onChange: sinon.stub(),
|
||||
tableNameSticky: true,
|
||||
tableName: '',
|
||||
database: { id: 1, database_name: 'main' },
|
||||
horizontal: false,
|
||||
sqlLabMode: true,
|
||||
clearable: false,
|
||||
handleError: sinon.stub(),
|
||||
};
|
||||
wrapper = shallow(<TableSelector {...mockedProps} />, {
|
||||
context: { store },
|
||||
});
|
||||
inst = wrapper.instance();
|
||||
beforeEach(async () => {
|
||||
fetchMock.reset();
|
||||
wrapper = await mountAndWait();
|
||||
});
|
||||
|
||||
it('is valid', () => {
|
||||
expect(React.isValidElement(<TableSelector {...mockedProps} />)).toBe(true);
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(TableSelector)).toExist();
|
||||
expect(wrapper.find(DatabaseSelector)).toExist();
|
||||
});
|
||||
|
||||
describe('onDatabaseChange', () => {
|
||||
it('should fetch schemas', () => {
|
||||
sinon.stub(inst, 'fetchSchemas');
|
||||
inst.onDatabaseChange({ id: 1 });
|
||||
expect(inst.fetchSchemas.getCall(0).args[0]).toBe(1);
|
||||
inst.fetchSchemas.restore();
|
||||
describe('change database', () => {
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should fetch schemas', async () => {
|
||||
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, { overwriteRoutes: true });
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
it('should clear tableOptions', () => {
|
||||
inst.onDatabaseChange();
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
|
||||
it('should fetch schema options', async () => {
|
||||
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
wrapper.update();
|
||||
expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
expect(
|
||||
wrapper.find('[name="select-schema"]').first().props().options,
|
||||
).toEqual([
|
||||
{ value: 'main', label: 'main', title: 'main' },
|
||||
{ value: 'erf', label: 'erf', title: 'erf' },
|
||||
{ value: 'superset', label: 'superset', title: 'superset' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should clear table options', async () => {
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
const props = wrapper.find('[name="async-select-table"]').first().props();
|
||||
expect(props.isDisabled).toBe(true);
|
||||
expect(props.value).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change schema', () => {
|
||||
beforeEach(async () => {
|
||||
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should fetch table', async () => {
|
||||
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, { overwriteRoutes: true });
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-schema"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedSchema);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should fetch table options', async () => {
|
||||
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-schema"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedSchema);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(
|
||||
wrapper.find('[name="select-schema"]').first().props().value[0],
|
||||
).toEqual(selectedSchema);
|
||||
expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
|
||||
const { options } = wrapper.find('[name="select-table"]').first().props();
|
||||
expect({ options }).toEqual(tables);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change table', () => {
|
||||
beforeEach(async () => {
|
||||
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should change table value', async () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-schema"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedSchema);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-table"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedTable);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(
|
||||
wrapper.find('[name="select-table"]').first().props().value,
|
||||
).toEqual('birth_names');
|
||||
});
|
||||
|
||||
it('should call onTableChange with schema from table object', async () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-schema"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedSchema);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-table"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedTable);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('birth_names');
|
||||
expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTableNamesBySubStr', () => {
|
||||
const GET_TABLE_NAMES_GLOB = 'glob:*/superset/tables/1/main/*';
|
||||
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should handle empty', () =>
|
||||
inst.getTableNamesBySubStr('').then(data => {
|
||||
expect(data).toEqual({ options: [] });
|
||||
return Promise.resolve();
|
||||
}));
|
||||
|
||||
it('should handle table name', () => {
|
||||
fetchMock.get(GET_TABLE_NAMES_GLOB, tables, { overwriteRoutes: true });
|
||||
|
||||
return wrapper
|
||||
.instance()
|
||||
.getTableNamesBySubStr('my table')
|
||||
.then(data => {
|
||||
expect(fetchMock.calls(GET_TABLE_NAMES_GLOB)).toHaveLength(1);
|
||||
expect(data).toEqual({
|
||||
options: [
|
||||
{
|
||||
value: 'birth_names',
|
||||
schema: 'main',
|
||||
label: 'birth_names',
|
||||
title: 'birth_names',
|
||||
},
|
||||
{
|
||||
value: 'energy_usage',
|
||||
schema: 'main',
|
||||
label: 'energy_usage',
|
||||
title: 'energy_usage',
|
||||
},
|
||||
{
|
||||
value: 'wb_health_population',
|
||||
schema: 'main',
|
||||
label: 'wb_health_population',
|
||||
title: 'wb_health_population',
|
||||
},
|
||||
],
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('should escape schema and table names', () => {
|
||||
const GET_TABLE_GLOB = 'glob:*/superset/tables/1/*/*';
|
||||
wrapper.setProps({ schema: 'slashed/schema' });
|
||||
fetchMock.get(GET_TABLE_GLOB, tables, { overwriteRoutes: true });
|
||||
|
||||
return wrapper
|
||||
.instance()
|
||||
.getTableNamesBySubStr('slashed/table')
|
||||
.then(() => {
|
||||
expect(fetchMock.lastUrl(GET_TABLE_GLOB)).toContain(
|
||||
'/slashed%252Fschema/slashed%252Ftable',
|
||||
);
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchTables', () => {
|
||||
const FETCH_TABLES_GLOB = 'glob:*/superset/tables/1/main/*/*/';
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should clear table options', () => {
|
||||
inst.fetchTables(true);
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
});
|
||||
|
||||
it('should fetch table options', () => {
|
||||
fetchMock.get(FETCH_TABLES_GLOB, tables, { overwriteRoutes: true });
|
||||
return inst.fetchTables(true, 'birth_names').then(() => {
|
||||
expect(wrapper.state().tableOptions).toHaveLength(3);
|
||||
expect(wrapper.state().tableOptions).toEqual([
|
||||
{
|
||||
value: 'birth_names',
|
||||
schema: 'main',
|
||||
label: 'birth_names',
|
||||
title: 'birth_names',
|
||||
},
|
||||
{
|
||||
value: 'energy_usage',
|
||||
schema: 'main',
|
||||
label: 'energy_usage',
|
||||
title: 'energy_usage',
|
||||
},
|
||||
{
|
||||
value: 'wb_health_population',
|
||||
schema: 'main',
|
||||
label: 'wb_health_population',
|
||||
title: 'wb_health_population',
|
||||
},
|
||||
]);
|
||||
return Promise.resolve();
|
||||
it('should handle empty', async () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="async-select-table"]')
|
||||
.first()
|
||||
.props()
|
||||
.loadOptions();
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
const props = wrapper.find('[name="async-select-table"]').first().props();
|
||||
expect(props.isDisabled).toBe(true);
|
||||
expect(props.value).toEqual('');
|
||||
});
|
||||
|
||||
// Test needs to be fixed: Github issue #7768
|
||||
it.skip('should dispatch a danger toast on error', () => {
|
||||
fetchMock.get(
|
||||
FETCH_TABLES_GLOB,
|
||||
{ throws: 'error' },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
|
||||
wrapper
|
||||
.instance()
|
||||
.fetchTables(true, 'birth_names')
|
||||
.then(() => {
|
||||
expect(wrapper.state().tableOptions).toEqual([]);
|
||||
expect(wrapper.state().tableOptions).toHaveLength(0);
|
||||
expect(mockedProps.handleError.callCount).toBe(1);
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchSchemas', () => {
|
||||
const FETCH_SCHEMAS_GLOB = 'glob:*/api/v1/database/*/schemas/?q=(force:!*)';
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should fetch schema options', () => {
|
||||
const schemaOptions = {
|
||||
result: ['main', 'erf', 'superset'],
|
||||
};
|
||||
fetchMock.get(FETCH_SCHEMAS_GLOB, schemaOptions, {
|
||||
it('should handle table name', async () => {
|
||||
wrapper.setProps({ schema: 'main' });
|
||||
fetchMock.get(GET_TABLE_ENDPOINT, tables, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
|
||||
return wrapper
|
||||
.instance()
|
||||
.fetchSchemas(1)
|
||||
.then(() => {
|
||||
expect(fetchMock.calls(FETCH_SCHEMAS_GLOB)).toHaveLength(1);
|
||||
expect(wrapper.state().schemaOptions).toHaveLength(3);
|
||||
});
|
||||
});
|
||||
|
||||
// Test needs to be fixed: Github issue #7768
|
||||
it.skip('should dispatch a danger toast on error', () => {
|
||||
const handleErrors = sinon.stub();
|
||||
expect(handleErrors.callCount).toBe(0);
|
||||
wrapper.setProps({ handleErrors });
|
||||
fetchMock.get(
|
||||
FETCH_SCHEMAS_GLOB,
|
||||
{ throws: new Error('Bad kitty') },
|
||||
{ overwriteRoutes: true },
|
||||
);
|
||||
wrapper
|
||||
.instance()
|
||||
.fetchSchemas(123)
|
||||
.then(() => {
|
||||
expect(wrapper.state().schemaOptions).toEqual([]);
|
||||
expect(handleErrors.callCount).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeTable', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(wrapper.instance(), 'fetchTables');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.instance().fetchTables.restore();
|
||||
});
|
||||
|
||||
it('test 1', () => {
|
||||
wrapper.instance().changeTable({
|
||||
value: 'birth_names',
|
||||
schema: 'main',
|
||||
label: 'birth_names',
|
||||
title: 'birth_names',
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="async-select-table"]')
|
||||
.first()
|
||||
.props()
|
||||
.loadOptions();
|
||||
});
|
||||
expect(wrapper.state().tableName).toBe('birth_names');
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(fetchMock.calls(GET_TABLE_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should call onTableChange with schema from table object', () => {
|
||||
wrapper.setProps({ schema: null });
|
||||
wrapper.instance().changeTable({
|
||||
value: 'my_table',
|
||||
schema: 'other_schema',
|
||||
label: 'other_schema.my_table',
|
||||
title: 'other_schema.my_table',
|
||||
});
|
||||
expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('my_table');
|
||||
expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('other_schema');
|
||||
});
|
||||
});
|
||||
|
||||
it('changeSchema', () => {
|
||||
sinon.stub(wrapper.instance(), 'fetchTables');
|
||||
|
||||
wrapper.instance().changeSchema({ label: 'main', value: 'main' });
|
||||
expect(wrapper.instance().fetchTables.callCount).toBe(1);
|
||||
expect(mockedProps.onChange.callCount).toBe(1);
|
||||
wrapper.instance().changeSchema();
|
||||
expect(wrapper.instance().fetchTables.callCount).toBe(2);
|
||||
expect(mockedProps.onChange.callCount).toBe(2);
|
||||
|
||||
wrapper.instance().fetchTables.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,12 +60,10 @@ async function mountAndWait(props = mockedProps) {
|
||||
}
|
||||
|
||||
describe('DatasourceModal', () => {
|
||||
fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD);
|
||||
const callsP = fetchMock.put(SAVE_ENDPOINT, SAVE_PAYLOAD);
|
||||
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.reset();
|
||||
wrapper = await mountAndWait();
|
||||
});
|
||||
|
||||
@@ -82,6 +80,7 @@ describe('DatasourceModal', () => {
|
||||
});
|
||||
|
||||
it('saves on confirm', async () => {
|
||||
const callsP = fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('button[data-test="datasource-modal-save"]')
|
||||
@@ -94,6 +93,9 @@ describe('DatasourceModal', () => {
|
||||
okButton.simulate('click');
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(callsP._calls).toHaveLength(2); /* eslint no-underscore-dangle: 0 */
|
||||
const expected = ['http://localhost/datasource/save/'];
|
||||
expect(callsP._calls.map(call => call[0])).toEqual(
|
||||
expected,
|
||||
); /* eslint no-underscore-dangle: 0 */
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,17 +120,18 @@ export default class SqlEditorLeftBar extends React.PureComponent {
|
||||
return (
|
||||
<div className="SqlEditorLeftBar">
|
||||
<TableSelector
|
||||
database={this.props.database}
|
||||
dbId={qe.dbId}
|
||||
schema={qe.schema}
|
||||
getDbList={this.getDbList}
|
||||
handleError={this.props.actions.addDangerToast}
|
||||
onDbChange={this.onDbChange}
|
||||
onSchemaChange={this.onSchemaChange}
|
||||
onSchemasLoad={this.onSchemasLoad}
|
||||
onTablesLoad={this.onTablesLoad}
|
||||
getDbList={this.getDbList}
|
||||
onTableChange={this.onTableChange}
|
||||
onTablesLoad={this.onTablesLoad}
|
||||
schema={qe.schema}
|
||||
sqlLabMode
|
||||
tableNameSticky={false}
|
||||
database={this.props.database}
|
||||
handleError={this.props.actions.addDangerToast}
|
||||
/>
|
||||
<div className="divider" />
|
||||
<div className="scrollbar-container">
|
||||
|
||||
283
superset-frontend/src/components/DatabaseSelector.tsx
Normal file
283
superset-frontend/src/components/DatabaseSelector.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* 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, { ReactNode, useEffect, useState } from 'react';
|
||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||
import rison from 'rison';
|
||||
import { Select } from 'src/components/Select';
|
||||
import Label from 'src/components/Label';
|
||||
|
||||
import SupersetAsyncSelect from './AsyncSelect';
|
||||
import RefreshLabel from './RefreshLabel';
|
||||
|
||||
const FieldTitle = styled.p`
|
||||
color: ${({ theme }) => theme.colors.secondary.light2};
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
margin: 20px 0 10px 0;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const DatabaseSelectorWrapper = styled.div`
|
||||
.fa-refresh {
|
||||
padding-left: 9px;
|
||||
}
|
||||
|
||||
.refresh-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
interface DatabaseSelectorProps {
|
||||
dbId: number;
|
||||
formMode?: boolean;
|
||||
getDbList?: (arg0: any) => {};
|
||||
getTableList?: (dbId: number, schema: string, force: boolean) => {};
|
||||
handleError: (msg: string) => void;
|
||||
isDatabaseSelectEnabled?: boolean;
|
||||
onDbChange?: (db: any) => void;
|
||||
onSchemaChange?: (arg0?: any) => {};
|
||||
onSchemasLoad?: (schemas: Array<object>) => void;
|
||||
schema?: string;
|
||||
sqlLabMode?: boolean;
|
||||
onChange?: ({
|
||||
dbId,
|
||||
schema,
|
||||
}: {
|
||||
dbId: number;
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function DatabaseSelector({
|
||||
dbId,
|
||||
formMode = false,
|
||||
getDbList,
|
||||
getTableList,
|
||||
handleError,
|
||||
isDatabaseSelectEnabled = true,
|
||||
onChange,
|
||||
onDbChange,
|
||||
onSchemaChange,
|
||||
onSchemasLoad,
|
||||
schema,
|
||||
sqlLabMode = false,
|
||||
}: DatabaseSelectorProps) {
|
||||
const [currentDbId, setCurrentDbId] = useState(dbId);
|
||||
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
|
||||
schema,
|
||||
);
|
||||
const [schemaLoading, setSchemaLoading] = useState(false);
|
||||
const [schemaOptions, setSchemaOptions] = useState([]);
|
||||
|
||||
function fetchSchemas(databaseId: number, forceRefresh = false) {
|
||||
const actualDbId = databaseId || dbId;
|
||||
if (actualDbId) {
|
||||
setSchemaLoading(true);
|
||||
const queryParams = rison.encode({
|
||||
force: Boolean(forceRefresh),
|
||||
});
|
||||
const endpoint = `/api/v1/database/${actualDbId}/schemas/?q=${queryParams}`;
|
||||
return SupersetClient.get({ endpoint })
|
||||
.then(({ json }) => {
|
||||
const options = json.result.map((s: string) => ({
|
||||
value: s,
|
||||
label: s,
|
||||
title: s,
|
||||
}));
|
||||
setSchemaOptions(options);
|
||||
setSchemaLoading(false);
|
||||
if (onSchemasLoad) {
|
||||
onSchemasLoad(options);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setSchemaOptions([]);
|
||||
setSchemaLoading(false);
|
||||
handleError(t('Error while fetching schema list'));
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDbId) {
|
||||
fetchSchemas(currentDbId);
|
||||
}
|
||||
}, [currentDbId]);
|
||||
|
||||
function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) {
|
||||
setCurrentDbId(dbId);
|
||||
setCurrentSchema(schema);
|
||||
if (onChange) {
|
||||
onChange({ dbId, schema, tableName: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
function dbMutator(data: any) {
|
||||
if (getDbList) {
|
||||
getDbList(data.result);
|
||||
}
|
||||
if (data.result.length === 0) {
|
||||
handleError(t("It seems you don't have access to any database"));
|
||||
}
|
||||
return data.result.map((row: any) => ({
|
||||
...row,
|
||||
// label is used for the typeahead
|
||||
label: `${row.backend} ${row.database_name}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function changeDataBase(db: any, force = false) {
|
||||
const dbId = db ? db.id : null;
|
||||
setSchemaOptions([]);
|
||||
if (onSchemaChange) {
|
||||
onSchemaChange(null);
|
||||
}
|
||||
if (onDbChange) {
|
||||
onDbChange(db);
|
||||
}
|
||||
fetchSchemas(dbId, force);
|
||||
onSelectChange({ dbId, schema: undefined });
|
||||
}
|
||||
|
||||
function changeSchema(schemaOpt: any, force = false) {
|
||||
const schema = schemaOpt ? schemaOpt.value : null;
|
||||
if (onSchemaChange) {
|
||||
onSchemaChange(schema);
|
||||
}
|
||||
setCurrentSchema(schema);
|
||||
onSelectChange({ dbId: currentDbId, schema });
|
||||
if (getTableList) {
|
||||
getTableList(currentDbId, schema, force);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDatabaseOption(db: any) {
|
||||
return (
|
||||
<span title={db.database_name}>
|
||||
<Label bsStyle="default">{db.backend}</Label>
|
||||
{db.database_name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
|
||||
return (
|
||||
<div className="section">
|
||||
<span className="select">{select}</span>
|
||||
<span className="refresh-col">{refreshBtn}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDatabaseSelect() {
|
||||
const queryParams = rison.encode({
|
||||
order_columns: 'database_name',
|
||||
order_direction: 'asc',
|
||||
page: 0,
|
||||
page_size: -1,
|
||||
...(formMode || !sqlLabMode
|
||||
? {}
|
||||
: {
|
||||
filters: [
|
||||
{
|
||||
col: 'expose_in_sqllab',
|
||||
opr: 'eq',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
return renderSelectRow(
|
||||
<SupersetAsyncSelect
|
||||
data-test="select-database"
|
||||
dataEndpoint={`/api/v1/database/?q=${queryParams}`}
|
||||
onChange={(db: any) => changeDataBase(db)}
|
||||
onAsyncError={() =>
|
||||
handleError(t('Error while fetching database list'))
|
||||
}
|
||||
clearable={false}
|
||||
value={currentDbId}
|
||||
valueKey="id"
|
||||
valueRenderer={(db: any) => (
|
||||
<div>
|
||||
<span className="text-muted m-r-5">{t('Database:')}</span>
|
||||
{renderDatabaseOption(db)}
|
||||
</div>
|
||||
)}
|
||||
optionRenderer={renderDatabaseOption}
|
||||
mutator={dbMutator}
|
||||
placeholder={t('Select a database')}
|
||||
autoSelect
|
||||
isDisabled={!isDatabaseSelectEnabled}
|
||||
/>,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
function renderSchemaSelect() {
|
||||
const value = schemaOptions.filter(({ value }) => currentSchema === value);
|
||||
const refresh = !formMode && (
|
||||
<RefreshLabel
|
||||
onClick={() => changeDataBase({ id: dbId }, true)}
|
||||
tooltipContent={t('Force refresh schema list')}
|
||||
/>
|
||||
);
|
||||
|
||||
return renderSelectRow(
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={t('Select a schema (%s)', schemaOptions.length)}
|
||||
options={schemaOptions}
|
||||
value={value}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">{t('Schema:')}</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
isLoading={schemaLoading}
|
||||
autosize={false}
|
||||
onChange={item => changeSchema(item)}
|
||||
/>,
|
||||
refresh,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DatabaseSelectorWrapper>
|
||||
{formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
|
||||
{renderDatabaseSelect()}
|
||||
{formMode && <FieldTitle>{t('schema')}</FieldTitle>}
|
||||
{renderSchemaSelect()}
|
||||
</DatabaseSelectorWrapper>
|
||||
);
|
||||
}
|
||||
@@ -1,446 +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 from 'react';
|
||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||
import PropTypes from 'prop-types';
|
||||
import rison from 'rison';
|
||||
import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
|
||||
|
||||
import Label from 'src/components/Label';
|
||||
import FormLabel from 'src/components/FormLabel';
|
||||
|
||||
import SupersetAsyncSelect from './AsyncSelect';
|
||||
import RefreshLabel from './RefreshLabel';
|
||||
import './TableSelector.less';
|
||||
|
||||
const FieldTitle = styled.p`
|
||||
color: ${({ theme }) => theme.colors.secondary.light2};
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
margin: 20px 0 10px 0;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const propTypes = {
|
||||
dbId: PropTypes.number.isRequired,
|
||||
schema: PropTypes.string,
|
||||
onSchemaChange: PropTypes.func,
|
||||
onDbChange: PropTypes.func,
|
||||
onSchemasLoad: PropTypes.func,
|
||||
onTablesLoad: PropTypes.func,
|
||||
getDbList: PropTypes.func,
|
||||
onTableChange: PropTypes.func,
|
||||
tableNameSticky: PropTypes.bool,
|
||||
tableName: PropTypes.string,
|
||||
database: PropTypes.object,
|
||||
sqlLabMode: PropTypes.bool,
|
||||
formMode: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
clearable: PropTypes.bool,
|
||||
handleError: PropTypes.func.isRequired,
|
||||
isDatabaseSelectEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
onDbChange: () => {},
|
||||
onSchemaChange: () => {},
|
||||
onSchemasLoad: () => {},
|
||||
onTablesLoad: () => {},
|
||||
getDbList: () => {},
|
||||
onTableChange: () => {},
|
||||
onChange: () => {},
|
||||
tableNameSticky: true,
|
||||
sqlLabMode: true,
|
||||
formMode: false,
|
||||
clearable: true,
|
||||
isDatabaseSelectEnabled: true,
|
||||
};
|
||||
|
||||
export default class TableSelector extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
schemaLoading: false,
|
||||
schemaOptions: [],
|
||||
tableLoading: false,
|
||||
tableOptions: [],
|
||||
dbId: props.dbId,
|
||||
schema: props.schema,
|
||||
tableName: props.tableName,
|
||||
};
|
||||
this.onDatabaseChange = this.onDatabaseChange.bind(this);
|
||||
this.onSchemaChange = this.onSchemaChange.bind(this);
|
||||
this.changeTable = this.changeTable.bind(this);
|
||||
this.dbMutator = this.dbMutator.bind(this);
|
||||
this.getTableNamesBySubStr = this.getTableNamesBySubStr.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.state.dbId) {
|
||||
this.fetchSchemas(this.state.dbId);
|
||||
this.fetchTables();
|
||||
}
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.props.onChange({
|
||||
dbId: this.state.dbId,
|
||||
schema: this.state.schema,
|
||||
tableName: this.state.tableName,
|
||||
});
|
||||
}
|
||||
|
||||
onDatabaseChange(db, force) {
|
||||
return this.changeDataBase(db, force);
|
||||
}
|
||||
|
||||
onSchemaChange(schemaOpt) {
|
||||
return this.changeSchema(schemaOpt);
|
||||
}
|
||||
|
||||
getTableNamesBySubStr(substr = 'undefined') {
|
||||
if (!this.props.dbId || !substr) {
|
||||
const options = [];
|
||||
return Promise.resolve({ options });
|
||||
}
|
||||
const encodedSchema = encodeURIComponent(this.props.schema);
|
||||
const encodedSubstr = encodeURIComponent(substr);
|
||||
return SupersetClient.get({
|
||||
endpoint: encodeURI(
|
||||
`/superset/tables/${this.props.dbId}/${encodedSchema}/${encodedSubstr}`,
|
||||
),
|
||||
}).then(({ json }) => {
|
||||
const options = json.options.map(o => ({
|
||||
value: o.value,
|
||||
schema: o.schema,
|
||||
label: o.label,
|
||||
title: o.title,
|
||||
type: o.type,
|
||||
}));
|
||||
return { options };
|
||||
});
|
||||
}
|
||||
|
||||
dbMutator(data) {
|
||||
this.props.getDbList(data.result);
|
||||
if (data.result.length === 0) {
|
||||
this.props.handleError(
|
||||
t("It seems you don't have access to any database"),
|
||||
);
|
||||
}
|
||||
return data.result.map(row => ({
|
||||
...row,
|
||||
// label is used for the typeahead
|
||||
label: `${row.backend} ${row.database_name}`,
|
||||
}));
|
||||
}
|
||||
|
||||
fetchTables(forceRefresh = false, substr = 'undefined') {
|
||||
const { dbId, schema } = this.state;
|
||||
const encodedSchema = encodeURIComponent(schema);
|
||||
const encodedSubstr = encodeURIComponent(substr);
|
||||
if (dbId && schema) {
|
||||
this.setState(() => ({ tableLoading: true, tableOptions: [] }));
|
||||
const endpoint = encodeURI(
|
||||
`/superset/tables/${dbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
|
||||
);
|
||||
return SupersetClient.get({ endpoint })
|
||||
.then(({ json }) => {
|
||||
const options = json.options.map(o => ({
|
||||
value: o.value,
|
||||
schema: o.schema,
|
||||
label: o.label,
|
||||
title: o.title,
|
||||
type: o.type,
|
||||
}));
|
||||
this.setState(() => ({
|
||||
tableLoading: false,
|
||||
tableOptions: options,
|
||||
}));
|
||||
this.props.onTablesLoad(json.options);
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState(() => ({ tableLoading: false, tableOptions: [] }));
|
||||
this.props.handleError(t('Error while fetching table list'));
|
||||
});
|
||||
}
|
||||
this.setState(() => ({ tableLoading: false, tableOptions: [] }));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
fetchSchemas(dbId, forceRefresh = false) {
|
||||
const actualDbId = dbId || this.props.dbId;
|
||||
if (actualDbId) {
|
||||
this.setState({ schemaLoading: true });
|
||||
const queryParams = rison.encode({
|
||||
force: Boolean(forceRefresh),
|
||||
});
|
||||
const endpoint = `/api/v1/database/${actualDbId}/schemas/?q=${queryParams}`;
|
||||
return SupersetClient.get({ endpoint })
|
||||
.then(({ json }) => {
|
||||
const schemaOptions = json.result.map(s => ({
|
||||
value: s,
|
||||
label: s,
|
||||
title: s,
|
||||
}));
|
||||
this.setState({ schemaOptions, schemaLoading: false });
|
||||
this.props.onSchemasLoad(schemaOptions);
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ schemaLoading: false, schemaOptions: [] });
|
||||
this.props.handleError(t('Error while fetching schema list'));
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
changeDataBase(db, force = false) {
|
||||
const dbId = db ? db.id : null;
|
||||
this.setState({ schemaOptions: [] });
|
||||
this.props.onSchemaChange(null);
|
||||
this.props.onDbChange(db);
|
||||
this.fetchSchemas(dbId, force);
|
||||
this.setState(
|
||||
{ dbId, schema: null, tableName: null, tableOptions: [] },
|
||||
this.onChange,
|
||||
);
|
||||
}
|
||||
|
||||
changeSchema(schemaOpt, force = false) {
|
||||
const schema = schemaOpt ? schemaOpt.value : null;
|
||||
this.props.onSchemaChange(schema);
|
||||
this.setState({ schema }, () => {
|
||||
this.fetchTables(force);
|
||||
this.onChange();
|
||||
});
|
||||
}
|
||||
|
||||
changeTable(tableOpt) {
|
||||
if (!tableOpt) {
|
||||
this.setState({ tableName: '' });
|
||||
return;
|
||||
}
|
||||
const schemaName = tableOpt.schema;
|
||||
const tableName = tableOpt.value;
|
||||
if (this.props.tableNameSticky) {
|
||||
this.setState({ tableName }, this.onChange);
|
||||
}
|
||||
this.props.onTableChange(tableName, schemaName);
|
||||
}
|
||||
|
||||
renderDatabaseOption(db) {
|
||||
return (
|
||||
<span title={db.database_name}>
|
||||
<Label bsStyle="default">{db.backend}</Label>
|
||||
{db.database_name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderTableOption(option) {
|
||||
return (
|
||||
<span className="TableLabel" title={option.label}>
|
||||
<span className="m-r-5">
|
||||
<small className="text-muted">
|
||||
<i
|
||||
className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`}
|
||||
/>
|
||||
</small>
|
||||
</span>
|
||||
{option.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
renderSelectRow(select, refreshBtn) {
|
||||
return (
|
||||
<div className="section">
|
||||
<span className="select">{select}</span>
|
||||
<span className="refresh-col">{refreshBtn}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderDatabaseSelect() {
|
||||
const queryParams = rison.encode({
|
||||
order_columns: 'database_name',
|
||||
order_direction: 'asc',
|
||||
page: 0,
|
||||
page_size: -1,
|
||||
...(this.props.formMode
|
||||
? {}
|
||||
: {
|
||||
filters: [
|
||||
{
|
||||
col: 'expose_in_sqllab',
|
||||
opr: 'eq',
|
||||
value: true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
return this.renderSelectRow(
|
||||
<SupersetAsyncSelect
|
||||
dataEndpoint={`/api/v1/database/?q=${queryParams}`}
|
||||
onChange={this.onDatabaseChange}
|
||||
onAsyncError={() =>
|
||||
this.props.handleError(t('Error while fetching database list'))
|
||||
}
|
||||
clearable={false}
|
||||
value={this.state.dbId}
|
||||
valueKey="id"
|
||||
valueRenderer={db => (
|
||||
<div>
|
||||
<span className="text-muted m-r-5">{t('Database:')}</span>
|
||||
{this.renderDatabaseOption(db)}
|
||||
</div>
|
||||
)}
|
||||
optionRenderer={this.renderDatabaseOption}
|
||||
mutator={this.dbMutator}
|
||||
placeholder={t('Select a database')}
|
||||
isDisabled={!this.props.isDatabaseSelectEnabled}
|
||||
autoSelect
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
renderSchema() {
|
||||
const refresh = !this.props.formMode && (
|
||||
<RefreshLabel
|
||||
onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
|
||||
tooltipContent={t('Force refresh schema list')}
|
||||
/>
|
||||
);
|
||||
return this.renderSelectRow(
|
||||
<Select
|
||||
name="select-schema"
|
||||
placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
|
||||
options={this.state.schemaOptions}
|
||||
value={this.props.schema}
|
||||
valueRenderer={o => (
|
||||
<div>
|
||||
<span className="text-muted">{t('Schema:')}</span> {o.label}
|
||||
</div>
|
||||
)}
|
||||
isLoading={this.state.schemaLoading}
|
||||
autosize={false}
|
||||
onChange={this.onSchemaChange}
|
||||
/>,
|
||||
refresh,
|
||||
);
|
||||
}
|
||||
|
||||
renderTable() {
|
||||
let tableSelectPlaceholder;
|
||||
let tableSelectDisabled = false;
|
||||
if (
|
||||
this.props.database &&
|
||||
this.props.database.allow_multi_schema_metadata_fetch
|
||||
) {
|
||||
tableSelectPlaceholder = t('Type to search ...');
|
||||
} else {
|
||||
tableSelectPlaceholder = t('Select table ');
|
||||
tableSelectDisabled = true;
|
||||
}
|
||||
const options = this.state.tableOptions;
|
||||
let select = null;
|
||||
if (this.props.schema && !this.props.formMode) {
|
||||
select = (
|
||||
<Select
|
||||
name="select-table"
|
||||
isLoading={this.state.tableLoading}
|
||||
ignoreAccents={false}
|
||||
placeholder={t('Select table or type table name')}
|
||||
autosize={false}
|
||||
onChange={this.changeTable}
|
||||
options={options}
|
||||
value={this.state.tableName}
|
||||
optionRenderer={this.renderTableOption}
|
||||
/>
|
||||
);
|
||||
} else if (this.props.formMode) {
|
||||
select = (
|
||||
<CreatableSelect
|
||||
name="select-table"
|
||||
isLoading={this.state.tableLoading}
|
||||
ignoreAccents={false}
|
||||
placeholder={t('Select table or type table name')}
|
||||
autosize={false}
|
||||
onChange={this.changeTable}
|
||||
options={options}
|
||||
value={this.state.tableName}
|
||||
optionRenderer={this.renderTableOption}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
select = (
|
||||
<AsyncSelect
|
||||
name="async-select-table"
|
||||
placeholder={tableSelectPlaceholder}
|
||||
isDisabled={tableSelectDisabled}
|
||||
autosize={false}
|
||||
onChange={this.changeTable}
|
||||
value={this.state.tableName}
|
||||
loadOptions={this.getTableNamesBySubStr}
|
||||
optionRenderer={this.renderTableOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const refresh = !this.props.formMode && (
|
||||
<RefreshLabel
|
||||
onClick={() => this.changeSchema({ value: this.props.schema }, true)}
|
||||
tooltipContent={t('Force refresh table list')}
|
||||
/>
|
||||
);
|
||||
return this.renderSelectRow(select, refresh);
|
||||
}
|
||||
|
||||
renderSeeTableLabel() {
|
||||
return (
|
||||
<div className="section">
|
||||
<FormLabel>
|
||||
{t('See table schema')}{' '}
|
||||
{this.props.schema && (
|
||||
<small>
|
||||
({this.state.tableOptions.length} in <i>{this.props.schema}</i>)
|
||||
</small>
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="TableSelector">
|
||||
{this.props.formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
|
||||
{this.renderDatabaseSelect()}
|
||||
{this.props.formMode && <FieldTitle>{t('schema')}</FieldTitle>}
|
||||
{this.renderSchema()}
|
||||
{!this.props.formMode && <div className="divider" />}
|
||||
{this.props.sqlLabMode && this.renderSeeTableLabel()}
|
||||
{this.props.formMode && <FieldTitle>{t('Table')}</FieldTitle>}
|
||||
{this.renderTable()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
TableSelector.propTypes = propTypes;
|
||||
TableSelector.defaultProps = defaultProps;
|
||||
379
superset-frontend/src/components/TableSelector.tsx
Normal file
379
superset-frontend/src/components/TableSelector.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* 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,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||
import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
|
||||
|
||||
import FormLabel from 'src/components/FormLabel';
|
||||
|
||||
import DatabaseSelector from './DatabaseSelector';
|
||||
import RefreshLabel from './RefreshLabel';
|
||||
|
||||
const FieldTitle = styled.p`
|
||||
color: ${({ theme }) => theme.colors.secondary.light2};
|
||||
font-size: ${({ theme }) => theme.typography.sizes.s}px;
|
||||
margin: 20px 0 10px 0;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const TableSelectorWrapper = styled.div`
|
||||
.fa-refresh {
|
||||
padding-left: 9px;
|
||||
}
|
||||
|
||||
.refresh-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.secondary.light5};
|
||||
margin: 15px 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const TableLabel = styled.span`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
interface TableSelectorProps {
|
||||
clearable?: boolean;
|
||||
database?: any;
|
||||
dbId: number;
|
||||
formMode?: boolean;
|
||||
getDbList?: (arg0: any) => {};
|
||||
handleError: (msg: string) => void;
|
||||
isDatabaseSelectEnabled?: boolean;
|
||||
onChange?: ({
|
||||
dbId,
|
||||
schema,
|
||||
}: {
|
||||
dbId: number;
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
}) => void;
|
||||
onDbChange?: (db: any) => void;
|
||||
onSchemaChange?: (arg0?: any) => {};
|
||||
onSchemasLoad?: () => void;
|
||||
onTableChange?: (tableName: string, schema: string) => void;
|
||||
onTablesLoad?: (options: Array<any>) => {};
|
||||
schema?: string;
|
||||
sqlLabMode?: boolean;
|
||||
tableName?: string;
|
||||
tableNameSticky?: boolean;
|
||||
}
|
||||
|
||||
const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
||||
database,
|
||||
dbId,
|
||||
formMode = false,
|
||||
getDbList,
|
||||
handleError,
|
||||
isDatabaseSelectEnabled = true,
|
||||
onChange,
|
||||
onDbChange,
|
||||
onSchemaChange,
|
||||
onSchemasLoad,
|
||||
onTableChange,
|
||||
onTablesLoad,
|
||||
schema,
|
||||
sqlLabMode = true,
|
||||
tableName,
|
||||
tableNameSticky = true,
|
||||
}) => {
|
||||
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
|
||||
schema,
|
||||
);
|
||||
const [currentTableName, setCurrentTableName] = useState<string | undefined>(
|
||||
tableName,
|
||||
);
|
||||
const [tableLoading, setTableLoading] = useState(false);
|
||||
const [tableOptions, setTableOptions] = useState([]);
|
||||
|
||||
function fetchTables(
|
||||
databaseId?: number,
|
||||
schema?: string,
|
||||
forceRefresh = false,
|
||||
substr = 'undefined',
|
||||
) {
|
||||
const dbSchema = schema || currentSchema;
|
||||
const actualDbId = databaseId || dbId;
|
||||
if (actualDbId && dbSchema) {
|
||||
const encodedSchema = encodeURIComponent(dbSchema);
|
||||
const encodedSubstr = encodeURIComponent(substr);
|
||||
setTableLoading(true);
|
||||
setTableOptions([]);
|
||||
const endpoint = encodeURI(
|
||||
`/superset/tables/${actualDbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
|
||||
);
|
||||
return SupersetClient.get({ endpoint })
|
||||
.then(({ json }) => {
|
||||
const options = json.options.map((o: any) => ({
|
||||
value: o.value,
|
||||
schema: o.schema,
|
||||
label: o.label,
|
||||
title: o.title,
|
||||
type: o.type,
|
||||
}));
|
||||
setTableLoading(false);
|
||||
setTableOptions(options);
|
||||
if (onTablesLoad) {
|
||||
onTablesLoad(json.options);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
setTableLoading(false);
|
||||
setTableOptions([]);
|
||||
handleError(t('Error while fetching table list'));
|
||||
});
|
||||
}
|
||||
setTableLoading(false);
|
||||
setTableOptions([]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dbId && schema) {
|
||||
fetchTables();
|
||||
}
|
||||
}, [dbId, schema]);
|
||||
|
||||
function onSelectionChange({
|
||||
dbId,
|
||||
schema,
|
||||
tableName,
|
||||
}: {
|
||||
dbId: number;
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
}) {
|
||||
setCurrentTableName(tableName);
|
||||
setCurrentSchema(schema);
|
||||
if (onChange) {
|
||||
onChange({ dbId, schema, tableName });
|
||||
}
|
||||
}
|
||||
|
||||
function getTableNamesBySubStr(substr = 'undefined') {
|
||||
if (!dbId || !substr) {
|
||||
const options: any[] = [];
|
||||
return Promise.resolve({ options });
|
||||
}
|
||||
const encodedSchema = encodeURIComponent(schema || '');
|
||||
const encodedSubstr = encodeURIComponent(substr);
|
||||
return SupersetClient.get({
|
||||
endpoint: encodeURI(
|
||||
`/superset/tables/${dbId}/${encodedSchema}/${encodedSubstr}`,
|
||||
),
|
||||
}).then(({ json }) => {
|
||||
const options = json.options.map((o: any) => ({
|
||||
value: o.value,
|
||||
schema: o.schema,
|
||||
label: o.label,
|
||||
title: o.title,
|
||||
type: o.type,
|
||||
}));
|
||||
return { options };
|
||||
});
|
||||
}
|
||||
|
||||
function changeTable(tableOpt: any) {
|
||||
if (!tableOpt) {
|
||||
setCurrentTableName('');
|
||||
return;
|
||||
}
|
||||
const schemaName = tableOpt.schema;
|
||||
const tableOptTableName = tableOpt.value;
|
||||
if (tableNameSticky) {
|
||||
onSelectionChange({
|
||||
dbId,
|
||||
schema: schemaName,
|
||||
tableName: tableOptTableName,
|
||||
});
|
||||
}
|
||||
if (onTableChange) {
|
||||
onTableChange(tableOptTableName, schemaName);
|
||||
}
|
||||
}
|
||||
|
||||
function changeSchema(schemaOpt: any, force = false) {
|
||||
const value = schemaOpt ? schemaOpt.value : null;
|
||||
if (onSchemaChange) {
|
||||
onSchemaChange(value);
|
||||
}
|
||||
onSelectionChange({
|
||||
dbId,
|
||||
schema: value,
|
||||
tableName: undefined,
|
||||
});
|
||||
fetchTables(dbId, currentSchema, force);
|
||||
}
|
||||
|
||||
function renderTableOption(option: any) {
|
||||
return (
|
||||
<TableLabel title={option.label}>
|
||||
<span className="m-r-5">
|
||||
<small className="text-muted">
|
||||
<i
|
||||
className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`}
|
||||
/>
|
||||
</small>
|
||||
</span>
|
||||
{option.label}
|
||||
</TableLabel>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
|
||||
return (
|
||||
<div className="section">
|
||||
<span className="select">{select}</span>
|
||||
<span className="refresh-col">{refreshBtn}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDatabaseSelector() {
|
||||
return (
|
||||
<DatabaseSelector
|
||||
dbId={dbId}
|
||||
formMode={formMode}
|
||||
getDbList={getDbList}
|
||||
getTableList={fetchTables}
|
||||
handleError={handleError}
|
||||
onChange={onSelectionChange}
|
||||
onDbChange={onDbChange}
|
||||
onSchemaChange={onSchemaChange}
|
||||
onSchemasLoad={onSchemasLoad}
|
||||
schema={currentSchema}
|
||||
sqlLabMode={sqlLabMode}
|
||||
isDatabaseSelectEnabled={isDatabaseSelectEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderTableSelect() {
|
||||
let tableSelectPlaceholder;
|
||||
let tableSelectDisabled = false;
|
||||
if (database && database.allow_multi_schema_metadata_fetch) {
|
||||
tableSelectPlaceholder = t('Type to search ...');
|
||||
} else {
|
||||
tableSelectPlaceholder = t('Select table ');
|
||||
tableSelectDisabled = true;
|
||||
}
|
||||
const options = tableOptions;
|
||||
let select = null;
|
||||
if (currentSchema && !formMode) {
|
||||
select = (
|
||||
<Select
|
||||
name="select-table"
|
||||
isLoading={tableLoading}
|
||||
ignoreAccents={false}
|
||||
placeholder={t('Select table or type table name')}
|
||||
autosize={false}
|
||||
onChange={changeTable}
|
||||
options={options}
|
||||
// @ts-ignore
|
||||
value={currentTableName}
|
||||
optionRenderer={renderTableOption}
|
||||
/>
|
||||
);
|
||||
} else if (formMode) {
|
||||
select = (
|
||||
<CreatableSelect
|
||||
name="select-table"
|
||||
isLoading={tableLoading}
|
||||
ignoreAccents={false}
|
||||
placeholder={t('Select table or type table name')}
|
||||
autosize={false}
|
||||
onChange={changeTable}
|
||||
options={options}
|
||||
// @ts-ignore
|
||||
value={currentTableName}
|
||||
optionRenderer={renderTableOption}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
select = (
|
||||
<AsyncSelect
|
||||
name="async-select-table"
|
||||
placeholder={tableSelectPlaceholder}
|
||||
isDisabled={tableSelectDisabled}
|
||||
autosize={false}
|
||||
onChange={changeTable}
|
||||
// @ts-ignore
|
||||
value={currentTableName}
|
||||
loadOptions={getTableNamesBySubStr}
|
||||
optionRenderer={renderTableOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const refresh = !formMode && (
|
||||
<RefreshLabel
|
||||
onClick={() => changeSchema({ value: schema }, true)}
|
||||
tooltipContent={t('Force refresh table list')}
|
||||
/>
|
||||
);
|
||||
return renderSelectRow(select, refresh);
|
||||
}
|
||||
|
||||
function renderSeeTableLabel() {
|
||||
return (
|
||||
<div className="section">
|
||||
<FormLabel>
|
||||
{t('See table schema')}{' '}
|
||||
{schema && (
|
||||
<small>
|
||||
{tableOptions.length} in
|
||||
<i>{schema}</i>
|
||||
</small>
|
||||
)}
|
||||
</FormLabel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableSelectorWrapper>
|
||||
{renderDatabaseSelector()}
|
||||
{!formMode && <div className="divider" />}
|
||||
{sqlLabMode && renderSeeTableLabel()}
|
||||
{formMode && <FieldTitle>{t('Table')}</FieldTitle>}
|
||||
{renderTableSelect()}
|
||||
</TableSelectorWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableSelector;
|
||||
@@ -18,15 +18,16 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert, Badge, Col, Tabs, Tab, Well } from 'react-bootstrap';
|
||||
import { Alert, Badge, Col, Radio, Tabs, Tab, Well } from 'react-bootstrap';
|
||||
import shortid from 'shortid';
|
||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||
|
||||
import Label from 'src/components/Label';
|
||||
import Button from 'src/components/Button';
|
||||
import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
|
||||
import DatabaseSelector from 'src/components/DatabaseSelector';
|
||||
import Label from 'src/components/Label';
|
||||
import Loading from 'src/components/Loading';
|
||||
import TableSelector from 'src/components/TableSelector';
|
||||
import CertifiedIconWithTooltip from 'src/components/CertifiedIconWithTooltip';
|
||||
import EditableTitle from 'src/components/EditableTitle';
|
||||
|
||||
import getClientErrorObject from 'src/utils/getClientErrorObject';
|
||||
@@ -74,6 +75,15 @@ const checkboxGenerator = (d, onChange) => (
|
||||
);
|
||||
const DATA_TYPES = ['STRING', 'NUMERIC', 'DATETIME'];
|
||||
|
||||
const DATASOURCE_TYPES_ARR = [
|
||||
{ key: 'physical', label: t('Physical (table or view)') },
|
||||
{ key: 'virtual', label: t('Virtual (SQL)') },
|
||||
];
|
||||
const DATASOURCE_TYPES = {};
|
||||
DATASOURCE_TYPES_ARR.forEach(o => {
|
||||
DATASOURCE_TYPES[o.key] = o;
|
||||
});
|
||||
|
||||
function CollectionTabTitle({ title, collection }) {
|
||||
return (
|
||||
<div>
|
||||
@@ -278,7 +288,10 @@ class DatasourceEditor extends React.PureComponent {
|
||||
col => !!col.expression,
|
||||
),
|
||||
metadataLoading: false,
|
||||
activeTabKey: 1,
|
||||
activeTabKey: 0,
|
||||
datasourceType: props.datasource.sql
|
||||
? DATASOURCE_TYPES.virtual.key
|
||||
: DATASOURCE_TYPES.physical.key,
|
||||
};
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
@@ -291,11 +304,19 @@ class DatasourceEditor extends React.PureComponent {
|
||||
}
|
||||
|
||||
onChange() {
|
||||
const datasource = {
|
||||
// Emptying SQL if "Physical" radio button is selected
|
||||
// Currently the logic to know whether the source is
|
||||
// physical or virtual is based on whether SQL is empty or not.
|
||||
const { datasourceType, datasource } = this.state;
|
||||
const sql =
|
||||
datasourceType === DATASOURCE_TYPES.physical.key ? '' : datasource.sql;
|
||||
|
||||
const newDatasource = {
|
||||
...this.state.datasource,
|
||||
sql,
|
||||
columns: [...this.state.databaseColumns, ...this.state.calculatedColumns],
|
||||
};
|
||||
this.props.onChange(datasource, this.state.errors);
|
||||
this.props.onChange(newDatasource, this.state.errors);
|
||||
}
|
||||
|
||||
onDatasourceChange(datasource) {
|
||||
@@ -310,6 +331,10 @@ class DatasourceEditor extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
onDatasourceTypeChange(datasourceType) {
|
||||
this.setState({ datasourceType });
|
||||
}
|
||||
|
||||
setColumns(obj) {
|
||||
this.setState(obj, this.validateAndChange);
|
||||
}
|
||||
@@ -466,34 +491,6 @@ class DatasourceEditor extends React.PureComponent {
|
||||
item={datasource}
|
||||
onChange={this.onDatasourceChange}
|
||||
>
|
||||
{this.state.isSqla && (
|
||||
<Field
|
||||
fieldKey="tableSelector"
|
||||
label={t('Physical Table')}
|
||||
control={
|
||||
<TableSelector
|
||||
dbId={datasource.database.id}
|
||||
schema={datasource.schema}
|
||||
tableName={datasource.datasource_name}
|
||||
onSchemaChange={schema =>
|
||||
this.onDatasourcePropChange('schema', schema)
|
||||
}
|
||||
onTableChange={table =>
|
||||
this.onDatasourcePropChange('datasource_name', table)
|
||||
}
|
||||
sqlLabMode={false}
|
||||
clearable={false}
|
||||
handleError={this.props.addDangerToast}
|
||||
isDatabaseSelectEnabled={false}
|
||||
/>
|
||||
}
|
||||
description={t(
|
||||
'The pointer to a physical table. Keep in mind that the chart is ' +
|
||||
'associated to this Superset logical table, and this logical table points ' +
|
||||
'the physical table referenced here.',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Field
|
||||
fieldKey="description"
|
||||
label={t('Description')}
|
||||
@@ -559,32 +556,6 @@ class DatasourceEditor extends React.PureComponent {
|
||||
item={datasource}
|
||||
onChange={this.onDatasourceChange}
|
||||
>
|
||||
{this.state.isSqla && (
|
||||
<Field
|
||||
fieldKey="sql"
|
||||
label={t('SQL')}
|
||||
description={t(
|
||||
'When specifying SQL, the dataset acts as a view. ' +
|
||||
'Superset will use this statement as a subquery while grouping and filtering ' +
|
||||
'on the generated parent queries.',
|
||||
)}
|
||||
control={
|
||||
<TextAreaControl language="sql" offerEditInModal={false} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{this.state.isDruid && (
|
||||
<Field
|
||||
fieldKey="json"
|
||||
label={t('JSON')}
|
||||
description={
|
||||
<div>{t('The JSON metric or post aggregation definition.')}</div>
|
||||
}
|
||||
control={
|
||||
<TextAreaControl language="json" offerEditInModal={false} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Field
|
||||
fieldKey="cache_timeout"
|
||||
label={t('Cache Timeout')}
|
||||
@@ -645,6 +616,121 @@ class DatasourceEditor extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderSourceFieldset() {
|
||||
const { datasource } = this.state;
|
||||
return (
|
||||
<div>
|
||||
<div className="m-l-10 m-t-20 m-b-10">
|
||||
{DATASOURCE_TYPES_ARR.map(type => (
|
||||
<Radio
|
||||
value={type.key}
|
||||
inline
|
||||
onChange={this.onDatasourceTypeChange.bind(this, type.key)}
|
||||
checked={this.state.datasourceType === type.key}
|
||||
>
|
||||
{type.label}
|
||||
</Radio>
|
||||
))}
|
||||
</div>
|
||||
<hr />
|
||||
<Fieldset item={datasource} onChange={this.onDatasourceChange} compact>
|
||||
{this.state.datasourceType === DATASOURCE_TYPES.virtual.key && (
|
||||
<div>
|
||||
{this.state.isSqla && (
|
||||
<>
|
||||
<Field
|
||||
fieldKey="databaseSelector"
|
||||
label={t('virtual')}
|
||||
control={
|
||||
<DatabaseSelector
|
||||
dbId={datasource.database.id}
|
||||
schema={datasource.schema}
|
||||
onSchemaChange={schema =>
|
||||
this.onDatasourcePropChange('schema', schema)
|
||||
}
|
||||
onDbChange={database =>
|
||||
this.onDatasourcePropChange('database', database)
|
||||
}
|
||||
formMode={false}
|
||||
handleError={this.props.addDangerToast}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Field
|
||||
fieldKey="sql"
|
||||
label={t('SQL')}
|
||||
description={t(
|
||||
'When specifying SQL, the datasource acts as a view. ' +
|
||||
'Superset will use this statement as a subquery while grouping and filtering ' +
|
||||
'on the generated parent queries.',
|
||||
)}
|
||||
control={
|
||||
<TextAreaControl
|
||||
language="sql"
|
||||
offerEditInModal={false}
|
||||
minLines={25}
|
||||
maxLines={25}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{this.state.isDruid && (
|
||||
<Field
|
||||
fieldKey="json"
|
||||
label={t('JSON')}
|
||||
description={
|
||||
<div>
|
||||
{t('The JSON metric or post aggregation definition.')}
|
||||
</div>
|
||||
}
|
||||
control={
|
||||
<TextAreaControl language="json" offerEditInModal={false} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{this.state.datasourceType === DATASOURCE_TYPES.physical.key && (
|
||||
<Col md={6}>
|
||||
{this.state.isSqla && (
|
||||
<Field
|
||||
fieldKey="tableSelector"
|
||||
label={t('Physical')}
|
||||
control={
|
||||
<TableSelector
|
||||
clearable={false}
|
||||
dbId={datasource.database.id}
|
||||
handleError={this.props.addDangerToast}
|
||||
schema={datasource.schema}
|
||||
sqlLabMode={false}
|
||||
tableName={datasource.table_name}
|
||||
onSchemaChange={schema =>
|
||||
this.onDatasourcePropChange('schema', schema)
|
||||
}
|
||||
onDbChange={database =>
|
||||
this.onDatasourcePropChange('database', database)
|
||||
}
|
||||
onTableChange={table => {
|
||||
this.onDatasourcePropChange('table_name', table);
|
||||
}}
|
||||
isDatabaseSelectEnabled={false}
|
||||
/>
|
||||
}
|
||||
description={t(
|
||||
'The pointer to a physical table (or view). Keep in mind that the chart is ' +
|
||||
'associated to this Superset logical table, and this logical table points ' +
|
||||
'the physical table referenced here.',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
</Fieldset>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderErrors() {
|
||||
if (this.state.errors.length > 0) {
|
||||
return (
|
||||
@@ -800,6 +886,9 @@ class DatasourceEditor extends React.PureComponent {
|
||||
onSelect={this.handleTabSelect}
|
||||
defaultActiveKey={activeTabKey}
|
||||
>
|
||||
<Tab eventKey={0} title={t('Source')}>
|
||||
{activeTabKey === 0 && this.renderSourceFieldset()}
|
||||
</Tab>
|
||||
<Tab
|
||||
title={
|
||||
<CollectionTabTitle
|
||||
|
||||
@@ -44,10 +44,8 @@ const StyledIcon = styled(Icon)`
|
||||
`;
|
||||
|
||||
const TableSelectorContainer = styled.div`
|
||||
.TableSelector {
|
||||
padding-bottom: 340px;
|
||||
width: 65%;
|
||||
}
|
||||
padding-bottom: 340px;
|
||||
width: 65%;
|
||||
`;
|
||||
|
||||
const DatasetModal: FunctionComponent<DatasetModalProps> = ({
|
||||
@@ -59,7 +57,7 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
|
||||
}) => {
|
||||
const [currentSchema, setSchema] = useState('');
|
||||
const [currentTableName, setTableName] = useState('');
|
||||
const [datasourceId, setDatasourceId] = useState<number | null>(null);
|
||||
const [datasourceId, setDatasourceId] = useState<number>(0);
|
||||
const [disableSave, setDisableSave] = useState(true);
|
||||
|
||||
const onChange = ({
|
||||
@@ -128,7 +126,6 @@ const DatasetModal: FunctionComponent<DatasetModalProps> = ({
|
||||
handleError={addDangerToast}
|
||||
onChange={onChange}
|
||||
schema={currentSchema}
|
||||
sqlLabMode={false}
|
||||
tableName={currentTableName}
|
||||
/>
|
||||
</TableSelectorContainer>
|
||||
|
||||
@@ -319,6 +319,10 @@ table.table-no-hover tr:hover {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.m-t-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.m-b-10 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -826,10 +826,6 @@ class TestDatasetApi(SupersetTestCase):
|
||||
self.login(username="admin")
|
||||
rv = self.get_assert_metric(uri, "export")
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
self.assertEqual(
|
||||
rv.headers["Content-Disposition"],
|
||||
generate_download_headers("yaml")["Content-Disposition"],
|
||||
)
|
||||
|
||||
cli_export = export_to_dict(
|
||||
session=db.session,
|
||||
|
||||
Reference in New Issue
Block a user