blob: 45d91f1c976620bdd016708536ac8abe3cbaf1a0 [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, { ReactNode, useState, useMemo, useEffect } from 'react';
import { styled, SupersetClient, t } from '@superset-ui/core';
import rison from 'rison';
import { Select } from 'src/components';
import Label from 'src/components/Label';
import { FormLabel } from 'src/components/Form';
import RefreshLabel from 'src/components/RefreshLabel';
const DatabaseSelectorWrapper = styled.div`
${({ theme }) => `
.refresh {
display: flex;
align-items: center;
width: 30px;
margin-left: ${theme.gridUnit}px;
margin-top: ${theme.gridUnit * 5}px;
}
.section {
display: flex;
flex-direction: row;
align-items: center;
}
.select {
flex: 1;
}
& > div {
margin-bottom: ${theme.gridUnit * 4}px;
}
`}
`;
const LabelStyle = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin-left: ${({ theme }) => theme.gridUnit - 2}px;
`;
type DatabaseValue = {
label: React.ReactNode;
value: number;
id: number;
database_name: string;
backend: string;
allow_multi_schema_metadata_fetch: boolean;
};
export type DatabaseObject = {
id: number;
database_name: string;
backend: string;
allow_multi_schema_metadata_fetch: boolean;
};
type SchemaValue = { label: string; value: string };
interface DatabaseSelectorProps {
db?: DatabaseObject;
formMode?: boolean;
getDbList?: (arg0: any) => {};
handleError: (msg: string) => void;
isDatabaseSelectEnabled?: boolean;
onDbChange?: (db: DatabaseObject) => void;
onSchemaChange?: (schema?: string) => void;
onSchemasLoad?: (schemas: Array<object>) => void;
readOnly?: boolean;
schema?: string;
sqlLabMode?: boolean;
}
const SelectLabel = ({
backend,
databaseName,
}: {
backend: string;
databaseName: string;
}) => (
<LabelStyle>
<Label>{backend}</Label>
{databaseName}
</LabelStyle>
);
export default function DatabaseSelector({
db,
formMode = false,
getDbList,
handleError,
isDatabaseSelectEnabled = true,
onDbChange,
onSchemaChange,
onSchemasLoad,
readOnly = false,
schema,
sqlLabMode = false,
}: DatabaseSelectorProps) {
const [loadingSchemas, setLoadingSchemas] = useState(false);
const [schemaOptions, setSchemaOptions] = useState<SchemaValue[]>([]);
const [currentDb, setCurrentDb] = useState<DatabaseValue | undefined>(
db
? {
label: (
<SelectLabel backend={db.backend} databaseName={db.database_name} />
),
value: db.id,
...db,
}
: undefined,
);
const [currentSchema, setCurrentSchema] = useState<SchemaValue | undefined>(
schema ? { label: schema, value: schema } : undefined,
);
const [refresh, setRefresh] = useState(0);
const loadDatabases = useMemo(
() => async (
search: string,
page: number,
pageSize: number,
): Promise<{
data: DatabaseValue[];
totalCount: number;
}> => {
const queryParams = rison.encode({
order_columns: 'database_name',
order_direction: 'asc',
page,
page_size: pageSize,
...(formMode || !sqlLabMode
? { filters: [{ col: 'database_name', opr: 'ct', value: search }] }
: {
filters: [
{ col: 'database_name', opr: 'ct', value: search },
{
col: 'expose_in_sqllab',
opr: 'eq',
value: true,
},
],
}),
});
const endpoint = `/api/v1/database/?q=${queryParams}`;
return SupersetClient.get({ endpoint }).then(({ json }) => {
const { result } = json;
if (getDbList) {
getDbList(result);
}
if (result.length === 0) {
handleError(t("It seems you don't have access to any database"));
}
const options = result.map((row: DatabaseObject) => ({
label: (
<SelectLabel
backend={row.backend}
databaseName={row.database_name}
/>
),
value: row.id,
id: row.id,
database_name: row.database_name,
backend: row.backend,
allow_multi_schema_metadata_fetch:
row.allow_multi_schema_metadata_fetch,
}));
return {
data: options,
totalCount: options.length,
};
});
},
[formMode, getDbList, handleError, sqlLabMode],
);
useEffect(() => {
if (currentDb) {
setLoadingSchemas(true);
const queryParams = rison.encode({ force: refresh > 0 });
const endpoint = `/api/v1/database/${currentDb.value}/schemas/?q=${queryParams}`;
// TODO: Would be nice to add pagination in a follow-up. Needs endpoint changes.
SupersetClient.get({ endpoint })
.then(({ json }) => {
const options = json.result.map((s: string) => ({
value: s,
label: s,
title: s,
}));
if (onSchemasLoad) {
onSchemasLoad(options);
}
setSchemaOptions(options);
setLoadingSchemas(false);
})
.catch(() => {
setLoadingSchemas(false);
handleError(t('There was an error loading the schemas'));
});
}
}, [currentDb, onSchemasLoad, refresh]);
function changeDataBase(
value: { label: string; value: number },
database: DatabaseValue,
) {
setCurrentDb(database);
setCurrentSchema(undefined);
if (onDbChange) {
onDbChange(database);
}
if (onSchemaChange) {
onSchemaChange(undefined);
}
}
function changeSchema(schema: SchemaValue) {
setCurrentSchema(schema);
if (onSchemaChange) {
onSchemaChange(schema.value);
}
}
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
return (
<div className="section">
<span className="select">{select}</span>
<span className="refresh">{refreshBtn}</span>
</div>
);
}
function renderDatabaseSelect() {
return renderSelectRow(
<Select
ariaLabel={t('Select database or type database name')}
optionFilterProps={['database_name', 'value']}
data-test="select-database"
header={<FormLabel>{t('Database')}</FormLabel>}
lazyLoading={false}
onChange={changeDataBase}
value={currentDb}
placeholder={t('Select database or type database name')}
disabled={!isDatabaseSelectEnabled || readOnly}
options={loadDatabases}
/>,
null,
);
}
function renderSchemaSelect() {
const refreshIcon = !formMode && !readOnly && (
<RefreshLabel
onClick={() => setRefresh(refresh + 1)}
tooltipContent={t('Force refresh schema list')}
/>
);
return renderSelectRow(
<Select
ariaLabel={t('Select schema or type schema name')}
disabled={readOnly}
header={<FormLabel>{t('Schema')}</FormLabel>}
labelInValue
lazyLoading={false}
loading={loadingSchemas}
name="select-schema"
placeholder={t('Select schema or type schema name')}
onChange={item => changeSchema(item as SchemaValue)}
options={schemaOptions}
showSearch
value={currentSchema}
/>,
refreshIcon,
);
}
return (
<DatabaseSelectorWrapper data-test="DatabaseSelector">
{renderDatabaseSelect()}
{renderSchemaSelect()}
</DatabaseSelectorWrapper>
);
}