refactor: Changes the DatabaseSelector and TableSelector to use the new Select component (#16483)

This commit is contained in:
Michael S. Molina
2021-09-22 07:43:46 -03:00
committed by GitHub
parent 1d5100daa2
commit 596e1cdf9b
18 changed files with 738 additions and 874 deletions

View File

@@ -26,11 +26,11 @@ import DatabaseSelector from '.';
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
const createProps = () => ({
dbId: 1,
db: { id: 1, database_name: 'test', backend: 'test-postgresql' },
formMode: false,
isDatabaseSelectEnabled: true,
readOnly: false,
schema: 'public',
schema: undefined,
sqlLabMode: true,
getDbList: jest.fn(),
getTableList: jest.fn(),
@@ -57,9 +57,9 @@ beforeEach(() => {
}
return {
json: {
count: 1,
count: 2,
description_columns: {},
ids: [1],
ids: [1, 2],
label_columns: {
allow_csv_upload: 'Allow Csv Upload',
allow_ctas: 'Allow Ctas',
@@ -129,12 +129,32 @@ beforeEach(() => {
changed_on: '2021-03-09T19:02:07.141095',
changed_on_delta_humanized: 'a day ago',
created_by: null,
database_name: 'examples',
database_name: 'test-postgres',
explore_database_id: 1,
expose_in_sqllab: true,
force_ctas_schema: null,
id: 1,
},
{
allow_csv_upload: false,
allow_ctas: false,
allow_cvas: false,
allow_dml: false,
allow_multi_schema_metadata_fetch: false,
allow_run_async: false,
allows_cost_estimate: null,
allows_subquery: true,
allows_virtual_table_explore: true,
backend: 'mysql',
changed_on: '2021-03-09T19:02:07.141095',
changed_on_delta_humanized: 'a day ago',
created_by: null,
database_name: 'test-mysql',
explore_database_id: 1,
expose_in_sqllab: true,
force_ctas_schema: null,
id: 2,
},
],
},
} as any;
@@ -153,50 +173,95 @@ test('Refresh should work', async () => {
render(<DatabaseSelector {...props} />);
const select = screen.getByRole('combobox', {
name: 'Select schema or type schema name',
});
userEvent.click(select);
await waitFor(() => {
expect(SupersetClientGet).toBeCalledTimes(2);
expect(props.getDbList).toBeCalledTimes(1);
expect(SupersetClientGet).toBeCalledTimes(1);
expect(props.getDbList).toBeCalledTimes(0);
expect(props.getTableList).toBeCalledTimes(0);
expect(props.handleError).toBeCalledTimes(0);
expect(props.onDbChange).toBeCalledTimes(0);
expect(props.onSchemaChange).toBeCalledTimes(0);
expect(props.onSchemasLoad).toBeCalledTimes(1);
expect(props.onSchemasLoad).toBeCalledTimes(0);
expect(props.onUpdate).toBeCalledTimes(0);
});
userEvent.click(screen.getByRole('button'));
userEvent.click(screen.getByRole('button', { name: 'refresh' }));
await waitFor(() => {
expect(SupersetClientGet).toBeCalledTimes(3);
expect(props.getDbList).toBeCalledTimes(1);
expect(SupersetClientGet).toBeCalledTimes(2);
expect(props.getDbList).toBeCalledTimes(0);
expect(props.getTableList).toBeCalledTimes(0);
expect(props.handleError).toBeCalledTimes(0);
expect(props.onDbChange).toBeCalledTimes(1);
expect(props.onSchemaChange).toBeCalledTimes(1);
expect(props.onDbChange).toBeCalledTimes(0);
expect(props.onSchemaChange).toBeCalledTimes(0);
expect(props.onSchemasLoad).toBeCalledTimes(2);
expect(props.onUpdate).toBeCalledTimes(1);
expect(props.onUpdate).toBeCalledTimes(0);
});
});
test('Should database select display options', async () => {
const props = createProps();
render(<DatabaseSelector {...props} />);
const selector = await screen.findByText('Database:');
expect(selector).toBeInTheDocument();
expect(selector.parentElement).toHaveTextContent(
'Database:postgresql examples',
);
const select = screen.getByRole('combobox', {
name: 'Select database or type database name',
});
expect(select).toBeInTheDocument();
userEvent.click(select);
expect(await screen.findByText('test-mysql')).toBeInTheDocument();
});
test('Should schema select display options', async () => {
const props = createProps();
render(<DatabaseSelector {...props} />);
const selector = await screen.findByText('Schema:');
expect(selector).toBeInTheDocument();
expect(selector.parentElement).toHaveTextContent('Schema: public');
userEvent.click(screen.getByRole('button'));
expect(await screen.findByText('Select a schema (2)')).toBeInTheDocument();
const select = screen.getByRole('combobox', {
name: 'Select schema or type schema name',
});
expect(select).toBeInTheDocument();
userEvent.click(select);
expect(
await screen.findByRole('option', { name: 'public' }),
).toBeInTheDocument();
expect(
await screen.findByRole('option', { name: 'information_schema' }),
).toBeInTheDocument();
});
test('Sends the correct db when changing the database', async () => {
const props = createProps();
render(<DatabaseSelector {...props} />);
const select = screen.getByRole('combobox', {
name: 'Select database or type database name',
});
expect(select).toBeInTheDocument();
userEvent.click(select);
userEvent.click(await screen.findByText('test-mysql'));
await waitFor(() =>
expect(props.onDbChange).toHaveBeenCalledWith(
expect.objectContaining({
id: 2,
database_name: 'test-mysql',
backend: 'mysql',
}),
),
);
});
test('Sends the correct schema when changing the schema', async () => {
const props = createProps();
render(<DatabaseSelector {...props} />);
const select = screen.getByRole('combobox', {
name: 'Select schema or type schema name',
});
expect(select).toBeInTheDocument();
userEvent.click(select);
const schemaOption = await screen.findAllByText('information_schema');
userEvent.click(schemaOption[1]);
await waitFor(() =>
expect(props.onSchemaChange).toHaveBeenCalledWith('information_schema'),
);
});

View File

@@ -16,80 +16,94 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { ReactNode, useEffect, useState } from 'react';
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/Select';
import { Select } from 'src/components';
import Label from 'src/components/Label';
import { FormLabel } from 'src/components/Form';
import RefreshLabel from 'src/components/RefreshLabel';
import SupersetAsyncSelect from 'src/components/AsyncSelect';
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;
}
${({ theme }) => `
.refresh {
display: flex;
align-items: center;
width: 30px;
margin-left: ${theme.gridUnit}px;
margin-top: ${theme.gridUnit * 5}px;
}
.refresh-col {
display: flex;
align-items: center;
width: 30px;
margin-left: ${({ theme }) => theme.gridUnit}px;
}
.section {
display: flex;
flex-direction: row;
align-items: center;
}
.section {
padding-bottom: 5px;
display: flex;
flex-direction: row;
}
.select {
flex: 1;
}
.select {
flex-grow: 1;
}
& > div {
margin-bottom: ${theme.gridUnit * 4}px;
}
`}
`;
const DatabaseOption = styled.span`
display: inline-flex;
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;
};
type SchemaValue = { label: string; value: string };
interface DatabaseSelectorProps {
dbId: number;
db?: { id: number; database_name: string; backend: string };
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) => {};
onDbChange?: (db: {
id: number;
database_name: string;
backend: string;
}) => void;
onSchemaChange?: (schema?: string) => void;
onSchemasLoad?: (schemas: Array<object>) => void;
readOnly?: boolean;
schema?: string;
sqlLabMode?: boolean;
onUpdate?: ({
dbId,
schema,
}: {
dbId: number;
schema?: string;
tableName?: string;
}) => void;
}
const SelectLabel = ({
backend,
databaseName,
}: {
backend: string;
databaseName: string;
}) => (
<LabelStyle>
<Label>{backend}</Label>
{databaseName}
</LabelStyle>
);
export default function DatabaseSelector({
dbId,
db,
formMode = false,
getDbList,
getTableList,
handleError,
isDatabaseSelectEnabled = true,
onUpdate,
onDbChange,
onSchemaChange,
onSchemasLoad,
@@ -97,193 +111,188 @@ export default function DatabaseSelector({
schema,
sqlLabMode = false,
}: DatabaseSelectorProps) {
const [currentDbId, setCurrentDbId] = useState(dbId);
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
schema,
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 [schemaLoading, setSchemaLoading] = useState(false);
const [schemaOptions, setSchemaOptions] = useState([]);
const [currentSchema, setCurrentSchema] = useState<SchemaValue | undefined>(
schema ? { label: schema, value: schema } : undefined,
);
const [refresh, setRefresh] = useState(0);
function fetchSchemas(databaseId: number, forceRefresh = false) {
const actualDbId = databaseId || dbId;
if (actualDbId) {
setSchemaLoading(true);
const loadDatabases = useMemo(
() => async (
search: string,
page: number,
pageSize: number,
): Promise<{
data: DatabaseValue[];
totalCount: number;
}> => {
const queryParams = rison.encode({
force: Boolean(forceRefresh),
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/${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);
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: { id: number; database_name: string; backend: string }) => ({
label: (
<SelectLabel
backend={row.backend}
databaseName={row.database_name}
/>
),
value: row.id,
id: row.id,
database_name: row.database_name,
backend: row.backend,
}),
);
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}`;
try {
// 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,
}))
.sort((a: { label: string }, b: { label: string }) =>
a.label.localeCompare(b.label),
);
if (onSchemasLoad) {
onSchemasLoad(options);
}
})
.catch(() => {
setSchemaOptions([]);
setSchemaLoading(false);
handleError(t('Error while fetching schema list'));
setSchemaOptions(options);
});
} finally {
setLoadingSchemas(false);
}
}
return Promise.resolve();
}
}, [currentDb, onSchemasLoad, refresh]);
useEffect(() => {
if (currentDbId) {
fetchSchemas(currentDbId);
}
}, [currentDbId]);
function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) {
setCurrentDbId(dbId);
setCurrentSchema(schema);
if (onUpdate) {
onUpdate({ 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);
}
function changeDataBase(
value: { label: string; value: number },
database: DatabaseValue,
) {
setCurrentDb(database);
setCurrentSchema(undefined);
if (onDbChange) {
onDbChange(db);
onDbChange(database);
}
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);
onSchemaChange(undefined);
}
}
function renderDatabaseOption(db: any) {
return (
<DatabaseOption title={db.database_name}>
<Label type="default">{db.backend}</Label> {db.database_name}
</DatabaseOption>
);
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-col">{refreshBtn}</span>
<span className="refresh">{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
<Select
ariaLabel={t('Select database or type database name')}
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 || readOnly}
header={<FormLabel>{t('Database')}</FormLabel>}
onChange={changeDataBase}
value={currentDb}
placeholder={t('Select database or type database name')}
disabled={!isDatabaseSelectEnabled || readOnly}
options={loadDatabases}
/>,
null,
);
}
function renderSchemaSelect() {
const value = schemaOptions.filter(({ value }) => currentSchema === value);
const refresh = !formMode && !readOnly && (
const refreshIcon = !formMode && !readOnly && (
<RefreshLabel
onClick={() => changeDataBase({ id: dbId }, true)}
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 a schema (%s)', schemaOptions.length)}
placeholder={t('Select schema or type schema name')}
onChange={item => changeSchema(item as SchemaValue)}
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)}
isDisabled={readOnly}
showSearch
value={currentSchema}
/>,
refresh,
refreshIcon,
);
}
return (
<DatabaseSelectorWrapper data-test="DatabaseSelector">
{formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
{renderDatabaseSelect()}
{formMode && <FieldTitle>{t('schema')}</FieldTitle>}
{renderSchemaSelect()}
</DatabaseSelectorWrapper>
);