mirror of
https://github.com/apache/superset.git
synced 2026-04-07 10:31:50 +00:00
* fix(log): log crashes if expired or not authenticated * fix lint and rison * add tests * more tests * perm fix * fix test not found * JS lint * fix Jest test
448 lines
13 KiB
JavaScript
448 lines
13 KiB
JavaScript
/**
|
|
* 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 from '@superset-ui/style';
|
|
import PropTypes from 'prop-types';
|
|
import rison from 'rison';
|
|
import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
|
|
import { Label } from 'react-bootstrap';
|
|
import { t } from '@superset-ui/translation';
|
|
import { SupersetClient } from '@superset-ui/connection';
|
|
|
|
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};
|
|
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,
|
|
};
|
|
|
|
const defaultProps = {
|
|
onDbChange: () => {},
|
|
onSchemaChange: () => {},
|
|
onSchemasLoad: () => {},
|
|
onTablesLoad: () => {},
|
|
getDbList: () => {},
|
|
onTableChange: () => {},
|
|
onChange: () => {},
|
|
tableNameSticky: true,
|
|
sqlLabMode: true,
|
|
formMode: false,
|
|
clearable: 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>
|
|
<Label bsStyle="default" className="m-r-5">
|
|
{db.backend}
|
|
</Label>
|
|
{db.database_name}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
renderTableOption(option) {
|
|
return (
|
|
<span className="TableLabel">
|
|
<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')}
|
|
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;
|