diff --git a/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx b/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx index 1ba1ac821fb1e..b153c148394ce 100644 --- a/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx +++ b/superset-frontend/spec/javascripts/sqllab/SqlEditorLeftBar_spec.jsx @@ -81,9 +81,13 @@ describe('Left Panel Expansion', () => { , ); - const dbSelect = screen.getByText(/select a database/i); - const schemaSelect = screen.getByText(/select a schema \(0\)/i); - const dropdown = screen.getByText(/Select table/i); + const dbSelect = screen.getByRole('combobox', { + name: 'Select a database', + }); + const schemaSelect = screen.getByRole('combobox', { + name: 'Select a schema', + }); + const dropdown = screen.getByText(/Select a table/i); const abUser = screen.getByText(/ab_user/i); expect(dbSelect).toBeInTheDocument(); expect(schemaSelect).toBeInTheDocument(); diff --git a/superset-frontend/src/components/CertifiedIcon/index.tsx b/superset-frontend/src/components/CertifiedIcon/index.tsx index f08e9bf6047ce..4aa0dad236b12 100644 --- a/superset-frontend/src/components/CertifiedIcon/index.tsx +++ b/superset-frontend/src/components/CertifiedIcon/index.tsx @@ -18,19 +18,19 @@ */ import React from 'react'; import { t, supersetTheme } from '@superset-ui/core'; -import Icons from 'src/components/Icons'; +import Icons, { IconType } from 'src/components/Icons'; import { Tooltip } from 'src/components/Tooltip'; export interface CertifiedIconProps { certifiedBy?: string; details?: string; - size?: number; + size?: IconType['iconSize']; } function CertifiedIcon({ certifiedBy, details, - size = 24, + size = 'l', }: CertifiedIconProps) { return ( ); diff --git a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx index 0d812824d1cf8..6d4abb3fd9634 100644 --- a/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx +++ b/superset-frontend/src/components/DatabaseSelector/DatabaseSelector.test.tsx @@ -26,11 +26,11 @@ import DatabaseSelector from '.'; const SupersetClientGet = jest.spyOn(SupersetClient, 'get'); const createProps = () => ({ - dbId: 1, + db: { id: 1, database_name: 'test', backend: 'postgresql' }, formMode: false, isDatabaseSelectEnabled: true, readOnly: false, - schema: 'public', + schema: undefined, sqlLabMode: true, getDbList: jest.fn(), getTableList: jest.fn(), @@ -129,7 +129,7 @@ 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', explore_database_id: 1, expose_in_sqllab: true, force_ctas_schema: null, @@ -153,50 +153,62 @@ test('Refresh should work', async () => { render(); + const select = screen.getByRole('combobox', { + name: 'Select a schema', + }); + + 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(); - const selector = await screen.findByText('Database:'); - expect(selector).toBeInTheDocument(); - expect(selector.parentElement).toHaveTextContent( - 'Database:postgresql examples', - ); + const select = screen.getByRole('combobox', { + name: 'Select a database', + }); + expect(select).toBeInTheDocument(); + userEvent.click(select); + expect( + await screen.findByRole('option', { name: 'postgresql: test' }), + ).toBeInTheDocument(); }); test('Should schema select display options', async () => { const props = createProps(); render(); - - 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 a schema', + }); + expect(select).toBeInTheDocument(); + userEvent.click(select); + expect( + await screen.findByRole('option', { name: 'public' }), + ).toBeInTheDocument(); + expect( + await screen.findByRole('option', { name: 'information_schema' }), + ).toBeInTheDocument(); }); diff --git a/superset-frontend/src/components/DatabaseSelector/index.tsx b/superset-frontend/src/components/DatabaseSelector/index.tsx index 0282e4a4a4c08..c96fba7c81e3c 100644 --- a/superset-frontend/src/components/DatabaseSelector/index.tsx +++ b/superset-frontend/src/components/DatabaseSelector/index.tsx @@ -16,58 +16,51 @@ * specific language governing permissions and limitations * under the License. */ -import React, { ReactNode, useEffect, useState } from 'react'; +import React, { ReactNode, useState, useMemo } 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 { Select } from 'src/components'; +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; - align-items: center; -`; +type DatabaseValue = { label: string; value: number }; + +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) => {}; + onSchemaChange?: (schema?: string) => void; onSchemasLoad?: (schemas: Array) => void; readOnly?: boolean; schema?: string; @@ -83,10 +76,9 @@ interface DatabaseSelectorProps { } export default function DatabaseSelector({ - dbId, + db, formMode = false, getDbList, - getTableList, handleError, isDatabaseSelectEnabled = true, onUpdate, @@ -97,193 +89,189 @@ export default function DatabaseSelector({ schema, sqlLabMode = false, }: DatabaseSelectorProps) { - const [currentDbId, setCurrentDbId] = useState(dbId); - const [currentSchema, setCurrentSchema] = useState( - schema, + const [currentDb, setCurrentDb] = useState( + db + ? { label: `${db.backend}: ${db.database_name}`, value: db.id } + : undefined, + ); + const [currentSchema, setCurrentSchema] = useState( + schema ? { label: schema, value: schema } : undefined, ); - const [schemaLoading, setSchemaLoading] = useState(false); - const [schemaOptions, setSchemaOptions] = useState([]); + const [refresh, setRefresh] = useState(0); - 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 loadSchemas = useMemo( + () => async (): Promise<{ + data: SchemaValue[]; + totalCount: number; + }> => { + if (currentDb) { + 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. + 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 { + data: options, + totalCount: options.length, + }; }); - } - return Promise.resolve(); - } - - useEffect(() => { - if (currentDbId) { - fetchSchemas(currentDbId); - } - }, [currentDbId]); + } + return { + data: [], + totalCount: 0, + }; + }, + [currentDb, refresh, onSchemasLoad], + ); - function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) { - setCurrentDbId(dbId); + function onSelectChange({ + db, + schema, + }: { + db: DatabaseValue; + schema?: SchemaValue; + }) { + setCurrentDb(db); 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")); + onUpdate({ + dbId: db.value, + schema: schema?.value, + tableName: undefined, + }); } - 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([]); + function changeDataBase(selectedValue: DatabaseValue) { + const actualDb = selectedValue || db; if (onSchemaChange) { - onSchemaChange(null); + onSchemaChange(undefined); } if (onDbChange) { onDbChange(db); } - fetchSchemas(dbId, force); - onSelectChange({ dbId, schema: undefined }); + onSelectChange({ db: actualDb, schema: undefined }); } - function changeSchema(schemaOpt: any, force = false) { - const schema = schemaOpt ? schemaOpt.value : null; + function changeSchema(schema: SchemaValue) { if (onSchemaChange) { - onSchemaChange(schema); + onSchemaChange(schema.value); } - setCurrentSchema(schema); - onSelectChange({ dbId: currentDbId, schema }); - if (getTableList) { - getTableList(currentDbId, schema, force); + if (currentDb) { + onSelectChange({ db: currentDb, schema }); } } - function renderDatabaseOption(db: any) { - return ( - - {db.database_name} - - ); - } - function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) { return (
{select} - {refreshBtn} + {refreshBtn}
); } - 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, - }, - ], + 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: { backend: string; database_name: string; id: number }) => ({ + label: `${row.backend}: ${row.database_name}`, + value: row.id, }), - }); + ); + return { + data: options, + totalCount: options.length, + }; + }); + }, + [formMode, getDbList, handleError, sqlLabMode], + ); + function renderDatabaseSelect() { return renderSelectRow( - changeDataBase(db)} - onAsyncError={() => - handleError(t('Error while fetching database list')) - } - clearable={false} - value={currentDbId} - valueKey="id" - valueRenderer={(db: any) => ( -
- {t('Database:')} - {renderDatabaseOption(db)} -
- )} - optionRenderer={renderDatabaseOption} - mutator={dbMutator} + header={{t('Database')}} + onChange={changeDataBase} + value={currentDb} placeholder={t('Select a database')} - autoSelect - isDisabled={!isDatabaseSelectEnabled || readOnly} + disabled={!isDatabaseSelectEnabled || readOnly} + options={loadDatabases} />, null, ); } function renderSchemaSelect() { - const value = schemaOptions.filter(({ value }) => currentSchema === value); - const refresh = !formMode && !readOnly && ( + const refreshIcon = !formMode && !readOnly && ( changeDataBase({ id: dbId }, true)} + onClick={() => setRefresh(refresh + 1)} tooltipContent={t('Force refresh schema list')} /> ); return renderSelectRow( - ); - } else if (formMode) { - select = ( - - ); - } 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 = ( - - ); - } + const disabled = + (currentSchema && !formMode && readOnly) || + (!currentSchema && !database?.allow_multi_schema_metadata_fetch); + + const header = sqlLabMode ? ( + {t('See table schema')} + ) : ( + {t('Table')} + ); + + const select = ( +