blob: c437b1e59a2c0c8e96ae8f5e2bcaed412f3c42e8 [file] [log] [blame]
/**
* 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/Form';
import DatabaseSelector from 'src/components/DatabaseSelector';
import RefreshLabel from 'src/components/RefreshLabel';
import CertifiedIcon from 'src/components/CertifiedIcon';
import WarningIconWithTooltip from 'src/components/WarningIconWithTooltip';
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;
margin-left: ${({ theme }) => theme.gridUnit}px;
}
.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;
}
.table-length {
color: ${({ theme }) => theme.colors.grayscale.light1};
}
`;
const TableLabel = styled.span`
align-items: center;
display: flex;
white-space: nowrap;
svg,
small {
margin-right: ${({ theme }) => theme.gridUnit}px;
}
`;
interface TableSelectorProps {
clearable?: boolean;
database?: any;
dbId: number;
formMode?: boolean;
getDbList?: (arg0: any) => {};
handleError: (msg: string) => void;
isDatabaseSelectEnabled?: boolean;
onUpdate?: ({
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>) => {};
readOnly?: boolean;
schema?: string;
sqlLabMode?: boolean;
tableName?: string;
tableNameSticky?: boolean;
}
const TableSelector: FunctionComponent<TableSelectorProps> = ({
database,
dbId,
formMode = false,
getDbList,
handleError,
isDatabaseSelectEnabled = true,
onUpdate,
onDbChange,
onSchemaChange,
onSchemasLoad,
onTableChange,
onTablesLoad,
readOnly = false,
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,
extra: o?.extra,
}));
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 (onUpdate) {
onUpdate({ 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}>
<small className="text-muted">
<i className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`} />
</small>
{option.extra?.certification && (
<CertifiedIcon
certifiedBy={option.extra.certification.certified_by}
details={option.extra.certification.details}
size={20}
/>
)}
{option.extra?.warning_markdown && (
<WarningIconWithTooltip
warningMarkdown={option.extra.warning_markdown}
size={20}
/>
)}
{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}
onUpdate={onSelectionChange}
onDbChange={readOnly ? undefined : onDbChange}
onSchemaChange={readOnly ? undefined : onSchemaChange}
onSchemasLoad={onSchemasLoad}
schema={currentSchema}
sqlLabMode={sqlLabMode}
isDatabaseSelectEnabled={isDatabaseSelectEnabled && !readOnly}
readOnly={readOnly}
/>
);
}
function renderTableSelect() {
const options = tableOptions;
let select = null;
if (currentSchema && !formMode) {
// dataset editor
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}
valueRenderer={renderTableOption}
isDisabled={readOnly}
/>
);
} 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 {
// sql lab
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;
}
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 && !readOnly && (
<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 className="table-length">
{tableOptions.length} in {schema}
</small>
)}
</FormLabel>
</div>
);
}
return (
<TableSelectorWrapper>
{renderDatabaseSelector()}
{!formMode && <div className="divider" />}
{sqlLabMode && renderSeeTableLabel()}
{formMode && <FieldTitle>{t('Table')}</FieldTitle>}
{renderTableSelect()}
</TableSelectorWrapper>
);
};
export default TableSelector;