mirror of https://github.com/apache/superset.git
This reverts commit c768941f2f
.
This commit is contained in:
parent
a413f796a6
commit
8adc31d14c
|
@ -81,13 +81,9 @@ describe('Left Panel Expansion', () => {
|
|||
</Provider>
|
||||
</ThemeProvider>,
|
||||
);
|
||||
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 dbSelect = screen.getByText(/select a database/i);
|
||||
const schemaSelect = screen.getByText(/select a schema \(0\)/i);
|
||||
const dropdown = screen.getByText(/Select table/i);
|
||||
const abUser = screen.getByText(/ab_user/i);
|
||||
expect(dbSelect).toBeInTheDocument();
|
||||
expect(schemaSelect).toBeInTheDocument();
|
||||
|
|
|
@ -18,19 +18,19 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { t, supersetTheme } from '@superset-ui/core';
|
||||
import Icons, { IconType } from 'src/components/Icons';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
|
||||
export interface CertifiedIconProps {
|
||||
certifiedBy?: string;
|
||||
details?: string;
|
||||
size?: IconType['iconSize'];
|
||||
size?: number;
|
||||
}
|
||||
|
||||
function CertifiedIcon({
|
||||
certifiedBy,
|
||||
details,
|
||||
size = 'l',
|
||||
size = 24,
|
||||
}: CertifiedIconProps) {
|
||||
return (
|
||||
<Tooltip
|
||||
|
@ -48,7 +48,8 @@ function CertifiedIcon({
|
|||
>
|
||||
<Icons.Certified
|
||||
iconColor={supersetTheme.colors.primary.base}
|
||||
iconSize={size}
|
||||
height={size}
|
||||
width={size}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
|
@ -26,11 +26,11 @@ import DatabaseSelector from '.';
|
|||
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
||||
|
||||
const createProps = () => ({
|
||||
db: { id: 1, database_name: 'test', backend: 'postgresql' },
|
||||
dbId: 1,
|
||||
formMode: false,
|
||||
isDatabaseSelectEnabled: true,
|
||||
readOnly: false,
|
||||
schema: undefined,
|
||||
schema: 'public',
|
||||
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: 'test',
|
||||
database_name: 'examples',
|
||||
explore_database_id: 1,
|
||||
expose_in_sqllab: true,
|
||||
force_ctas_schema: null,
|
||||
|
@ -153,62 +153,50 @@ test('Refresh should work', async () => {
|
|||
|
||||
render(<DatabaseSelector {...props} />);
|
||||
|
||||
const select = screen.getByRole('combobox', {
|
||||
name: 'Select a schema',
|
||||
});
|
||||
|
||||
userEvent.click(select);
|
||||
|
||||
await waitFor(() => {
|
||||
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(0);
|
||||
expect(props.onUpdate).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button', { name: 'refresh' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(SupersetClientGet).toBeCalledTimes(2);
|
||||
expect(props.getDbList).toBeCalledTimes(0);
|
||||
expect(props.getDbList).toBeCalledTimes(1);
|
||||
expect(props.getTableList).toBeCalledTimes(0);
|
||||
expect(props.handleError).toBeCalledTimes(0);
|
||||
expect(props.onDbChange).toBeCalledTimes(0);
|
||||
expect(props.onSchemaChange).toBeCalledTimes(0);
|
||||
expect(props.onSchemasLoad).toBeCalledTimes(2);
|
||||
expect(props.onSchemasLoad).toBeCalledTimes(1);
|
||||
expect(props.onUpdate).toBeCalledTimes(0);
|
||||
});
|
||||
|
||||
userEvent.click(screen.getByRole('button'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(SupersetClientGet).toBeCalledTimes(3);
|
||||
expect(props.getDbList).toBeCalledTimes(1);
|
||||
expect(props.getTableList).toBeCalledTimes(0);
|
||||
expect(props.handleError).toBeCalledTimes(0);
|
||||
expect(props.onDbChange).toBeCalledTimes(1);
|
||||
expect(props.onSchemaChange).toBeCalledTimes(1);
|
||||
expect(props.onSchemasLoad).toBeCalledTimes(2);
|
||||
expect(props.onUpdate).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Should database select display options', async () => {
|
||||
const props = createProps();
|
||||
render(<DatabaseSelector {...props} />);
|
||||
const select = screen.getByRole('combobox', {
|
||||
name: 'Select a database',
|
||||
});
|
||||
expect(select).toBeInTheDocument();
|
||||
userEvent.click(select);
|
||||
expect(
|
||||
await screen.findByRole('option', { name: 'postgresql: test' }),
|
||||
).toBeInTheDocument();
|
||||
const selector = await screen.findByText('Database:');
|
||||
expect(selector).toBeInTheDocument();
|
||||
expect(selector.parentElement).toHaveTextContent(
|
||||
'Database:postgresql examples',
|
||||
);
|
||||
});
|
||||
|
||||
test('Should schema select display options', async () => {
|
||||
const props = createProps();
|
||||
render(<DatabaseSelector {...props} />);
|
||||
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();
|
||||
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -16,51 +16,58 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { ReactNode, useState, useMemo } from 'react';
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||
import rison from 'rison';
|
||||
import { Select } from 'src/components';
|
||||
import { FormLabel } from 'src/components/Form';
|
||||
import { Select } from 'src/components/Select';
|
||||
import Label from 'src/components/Label';
|
||||
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`
|
||||
${({ theme }) => `
|
||||
.refresh {
|
||||
.fa-refresh {
|
||||
padding-left: 9px;
|
||||
}
|
||||
|
||||
.refresh-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
margin-left: ${theme.gridUnit}px;
|
||||
margin-top: ${theme.gridUnit * 5}px;
|
||||
margin-left: ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
flex: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
& > div {
|
||||
margin-bottom: ${theme.gridUnit * 4}px;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
type DatabaseValue = { label: string; value: number };
|
||||
|
||||
type SchemaValue = { label: string; value: string };
|
||||
const DatabaseOption = styled.span`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface DatabaseSelectorProps {
|
||||
db?: { id: number; database_name: string; backend: string };
|
||||
dbId: number;
|
||||
formMode?: boolean;
|
||||
getDbList?: (arg0: any) => {};
|
||||
getTableList?: (dbId: number, schema: string, force: boolean) => {};
|
||||
handleError: (msg: string) => void;
|
||||
isDatabaseSelectEnabled?: boolean;
|
||||
onDbChange?: (db: any) => void;
|
||||
onSchemaChange?: (schema?: string) => void;
|
||||
onSchemaChange?: (arg0?: any) => {};
|
||||
onSchemasLoad?: (schemas: Array<object>) => void;
|
||||
readOnly?: boolean;
|
||||
schema?: string;
|
||||
|
@ -76,9 +83,10 @@ interface DatabaseSelectorProps {
|
|||
}
|
||||
|
||||
export default function DatabaseSelector({
|
||||
db,
|
||||
dbId,
|
||||
formMode = false,
|
||||
getDbList,
|
||||
getTableList,
|
||||
handleError,
|
||||
isDatabaseSelectEnabled = true,
|
||||
onUpdate,
|
||||
|
@ -89,115 +97,123 @@ export default function DatabaseSelector({
|
|||
schema,
|
||||
sqlLabMode = false,
|
||||
}: DatabaseSelectorProps) {
|
||||
const [currentDb, setCurrentDb] = useState(
|
||||
db
|
||||
? { label: `${db.backend}: ${db.database_name}`, value: db.id }
|
||||
: undefined,
|
||||
const [currentDbId, setCurrentDbId] = useState(dbId);
|
||||
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
|
||||
schema,
|
||||
);
|
||||
const [currentSchema, setCurrentSchema] = useState<SchemaValue | undefined>(
|
||||
schema ? { label: schema, value: schema } : undefined,
|
||||
);
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const [schemaLoading, setSchemaLoading] = useState(false);
|
||||
const [schemaOptions, setSchemaOptions] = useState([]);
|
||||
|
||||
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 }) => {
|
||||
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 options = json.result.map((s: string) => ({
|
||||
value: s,
|
||||
label: s,
|
||||
title: s,
|
||||
}));
|
||||
setSchemaOptions(options);
|
||||
setSchemaLoading(false);
|
||||
if (onSchemasLoad) {
|
||||
onSchemasLoad(options);
|
||||
}
|
||||
return {
|
||||
data: options,
|
||||
totalCount: options.length,
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
setSchemaOptions([]);
|
||||
setSchemaLoading(false);
|
||||
handleError(t('Error while fetching schema list'));
|
||||
});
|
||||
}
|
||||
return {
|
||||
data: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
},
|
||||
[currentDb, refresh, onSchemasLoad],
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function onSelectChange({
|
||||
db,
|
||||
schema,
|
||||
}: {
|
||||
db: DatabaseValue;
|
||||
schema?: SchemaValue;
|
||||
}) {
|
||||
setCurrentDb(db);
|
||||
useEffect(() => {
|
||||
if (currentDbId) {
|
||||
fetchSchemas(currentDbId);
|
||||
}
|
||||
}, [currentDbId]);
|
||||
|
||||
function onSelectChange({ dbId, schema }: { dbId: number; schema?: string }) {
|
||||
setCurrentDbId(dbId);
|
||||
setCurrentSchema(schema);
|
||||
if (onUpdate) {
|
||||
onUpdate({
|
||||
dbId: db.value,
|
||||
schema: schema?.value,
|
||||
tableName: undefined,
|
||||
});
|
||||
onUpdate({ dbId, schema, tableName: undefined });
|
||||
}
|
||||
}
|
||||
|
||||
function changeDataBase(selectedValue: DatabaseValue) {
|
||||
const actualDb = selectedValue || db;
|
||||
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(undefined);
|
||||
onSchemaChange(null);
|
||||
}
|
||||
if (onDbChange) {
|
||||
onDbChange(db);
|
||||
}
|
||||
onSelectChange({ db: actualDb, schema: undefined });
|
||||
fetchSchemas(dbId, force);
|
||||
onSelectChange({ dbId, schema: undefined });
|
||||
}
|
||||
|
||||
function changeSchema(schema: SchemaValue) {
|
||||
function changeSchema(schemaOpt: any, force = false) {
|
||||
const schema = schemaOpt ? schemaOpt.value : null;
|
||||
if (onSchemaChange) {
|
||||
onSchemaChange(schema.value);
|
||||
onSchemaChange(schema);
|
||||
}
|
||||
if (currentDb) {
|
||||
onSelectChange({ db: currentDb, schema });
|
||||
setCurrentSchema(schema);
|
||||
onSelectChange({ dbId: currentDbId, schema });
|
||||
if (getTableList) {
|
||||
getTableList(currentDbId, schema, force);
|
||||
}
|
||||
}
|
||||
|
||||
function renderDatabaseOption(db: any) {
|
||||
return (
|
||||
<DatabaseOption title={db.database_name}>
|
||||
<Label type="default">{db.backend}</Label> {db.database_name}
|
||||
</DatabaseOption>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
|
||||
return (
|
||||
<div className="section">
|
||||
<span className="select">{select}</span>
|
||||
<span className="refresh">{refreshBtn}</span>
|
||||
<span className="refresh-col">{refreshBtn}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const loadDatabases = useMemo(
|
||||
() => async (
|
||||
search: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<{
|
||||
data: DatabaseValue[];
|
||||
totalCount: number;
|
||||
}> => {
|
||||
function renderDatabaseSelect() {
|
||||
const queryParams = rison.encode({
|
||||
order_columns: 'database_name',
|
||||
order_direction: 'asc',
|
||||
page,
|
||||
page_size: pageSize,
|
||||
page: 0,
|
||||
page_size: -1,
|
||||
...(formMode || !sqlLabMode
|
||||
? { filters: [{ col: 'database_name', opr: 'ct', value: search }] }
|
||||
? {}
|
||||
: {
|
||||
filters: [
|
||||
{ col: 'database_name', opr: 'ct', value: search },
|
||||
{
|
||||
col: 'expose_in_sqllab',
|
||||
opr: 'eq',
|
||||
|
@ -206,72 +222,68 @@ export default function DatabaseSelector({
|
|||
],
|
||||
}),
|
||||
});
|
||||
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(
|
||||
<Select
|
||||
ariaLabel={t('Select a database')}
|
||||
<SupersetAsyncSelect
|
||||
data-test="select-database"
|
||||
header={<FormLabel>{t('Database')}</FormLabel>}
|
||||
onChange={changeDataBase}
|
||||
value={currentDb}
|
||||
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')}
|
||||
disabled={!isDatabaseSelectEnabled || readOnly}
|
||||
options={loadDatabases}
|
||||
autoSelect
|
||||
isDisabled={!isDatabaseSelectEnabled || readOnly}
|
||||
/>,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
function renderSchemaSelect() {
|
||||
const refreshIcon = !formMode && !readOnly && (
|
||||
const value = schemaOptions.filter(({ value }) => currentSchema === value);
|
||||
const refresh = !formMode && !readOnly && (
|
||||
<RefreshLabel
|
||||
onClick={() => setRefresh(refresh + 1)}
|
||||
onClick={() => changeDataBase({ id: dbId }, true)}
|
||||
tooltipContent={t('Force refresh schema list')}
|
||||
/>
|
||||
);
|
||||
|
||||
return renderSelectRow(
|
||||
<Select
|
||||
ariaLabel={t('Select a schema')}
|
||||
disabled={readOnly}
|
||||
header={<FormLabel>{t('Schema')}</FormLabel>}
|
||||
name="select-schema"
|
||||
placeholder={t('Select a schema')}
|
||||
onChange={item => changeSchema(item as SchemaValue)}
|
||||
options={loadSchemas}
|
||||
value={currentSchema}
|
||||
placeholder={t('Select a schema (%s)', schemaOptions.length)}
|
||||
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}
|
||||
/>,
|
||||
refreshIcon,
|
||||
refresh,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DatabaseSelectorWrapper data-test="DatabaseSelector">
|
||||
{formMode && <FieldTitle>{t('datasource')}</FieldTitle>}
|
||||
{renderDatabaseSelect()}
|
||||
{formMode && <FieldTitle>{t('schema')}</FieldTitle>}
|
||||
{renderSchemaSelect()}
|
||||
</DatabaseSelectorWrapper>
|
||||
);
|
||||
|
|
|
@ -53,21 +53,15 @@ export const Icon = (props: IconProps) => {
|
|||
const name = fileName.replace('_', '-');
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
async function importIcon(): Promise<void> {
|
||||
ImportedSVG.current = (
|
||||
await import(
|
||||
`!!@svgr/webpack?-svgo,+titleProp,+ref!images/icons/${fileName}.svg`
|
||||
)
|
||||
).default;
|
||||
if (!cancelled) {
|
||||
setLoaded(true);
|
||||
}
|
||||
}
|
||||
importIcon();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [fileName, ImportedSVG]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -86,9 +86,12 @@ const StyledContainer = styled.div`
|
|||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledSelect = styled(AntdSelect)`
|
||||
${({ theme }) => `
|
||||
const StyledSelect = styled(AntdSelect, {
|
||||
shouldForwardProp: prop => prop !== 'hasHeader',
|
||||
})<{ hasHeader: boolean }>`
|
||||
${({ theme, hasHeader }) => `
|
||||
width: 100%;
|
||||
margin-top: ${hasHeader ? theme.gridUnit : 0}px;
|
||||
|
||||
&& .ant-select-selector {
|
||||
border-radius: ${theme.gridUnit}px;
|
||||
|
@ -186,7 +189,6 @@ const Select = ({
|
|||
: 'multiple';
|
||||
|
||||
useEffect(() => {
|
||||
fetchedQueries.current.clear();
|
||||
setSelectOptions(
|
||||
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
|
||||
);
|
||||
|
@ -364,14 +366,11 @@ const Select = ({
|
|||
[options],
|
||||
);
|
||||
|
||||
const handleOnSearch = useMemo(
|
||||
() =>
|
||||
debounce((search: string) => {
|
||||
const handleOnSearch = debounce((search: string) => {
|
||||
const searchValue = search.trim();
|
||||
// enables option creation
|
||||
if (allowNewOptions && isSingleMode) {
|
||||
const firstOption =
|
||||
selectOptions.length > 0 && selectOptions[0].value;
|
||||
const firstOption = selectOptions.length > 0 && selectOptions[0].value;
|
||||
// replaces the last search value entered with the new one
|
||||
// only when the value wasn't part of the original options
|
||||
if (
|
||||
|
@ -394,15 +393,7 @@ const Select = ({
|
|||
}
|
||||
}
|
||||
setSearchedValue(searchValue);
|
||||
}, DEBOUNCE_TIMEOUT),
|
||||
[
|
||||
allowNewOptions,
|
||||
initialOptions,
|
||||
isSingleMode,
|
||||
searchedValue,
|
||||
selectOptions,
|
||||
],
|
||||
);
|
||||
}, DEBOUNCE_TIMEOUT);
|
||||
|
||||
const handlePagination = (e: UIEvent<HTMLElement>) => {
|
||||
const vScroll = e.currentTarget;
|
||||
|
@ -495,6 +486,7 @@ const Select = ({
|
|||
<StyledContainer>
|
||||
{header}
|
||||
<StyledSelect
|
||||
hasHeader={!!header}
|
||||
aria-label={ariaLabel || name}
|
||||
dropdownRender={dropdownRender}
|
||||
filterOption={handleFilterOption}
|
||||
|
|
|
@ -0,0 +1,291 @@
|
|||
/**
|
||||
* 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 configureStore from 'redux-mock-store';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import sinon from 'sinon';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import thunk from 'redux-thunk';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
|
||||
|
||||
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
|
||||
|
||||
import DatabaseSelector from 'src/components/DatabaseSelector';
|
||||
import TableSelector from 'src/components/TableSelector';
|
||||
import { initialState, tables } from 'spec/javascripts/sqllab/fixtures';
|
||||
|
||||
const mockStore = configureStore([thunk]);
|
||||
const store = mockStore(initialState);
|
||||
|
||||
const FETCH_SCHEMAS_ENDPOINT = 'glob:*/api/v1/database/*/schemas/*';
|
||||
const GET_TABLE_ENDPOINT = 'glob:*/superset/tables/1/*/*';
|
||||
const GET_TABLE_NAMES_ENDPOINT = 'glob:*/superset/tables/1/main/*';
|
||||
|
||||
const mockedProps = {
|
||||
clearable: false,
|
||||
database: { id: 1, database_name: 'main' },
|
||||
dbId: 1,
|
||||
formMode: false,
|
||||
getDbList: sinon.stub(),
|
||||
handleError: sinon.stub(),
|
||||
horizontal: false,
|
||||
onChange: sinon.stub(),
|
||||
onDbChange: sinon.stub(),
|
||||
onSchemaChange: sinon.stub(),
|
||||
onTableChange: sinon.stub(),
|
||||
sqlLabMode: true,
|
||||
tableName: '',
|
||||
tableNameSticky: true,
|
||||
};
|
||||
|
||||
const schemaOptions = {
|
||||
result: ['main', 'erf', 'superset'],
|
||||
};
|
||||
const selectedSchema = { label: 'main', title: 'main', value: 'main' };
|
||||
const selectedTable = {
|
||||
extra: null,
|
||||
label: 'birth_names',
|
||||
schema: 'main',
|
||||
title: 'birth_names',
|
||||
type: undefined,
|
||||
value: 'birth_names',
|
||||
};
|
||||
|
||||
async function mountAndWait(props = mockedProps) {
|
||||
const mounted = mount(<TableSelector {...props} />, {
|
||||
context: { store },
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
});
|
||||
await waitForComponentToPaint(mounted);
|
||||
|
||||
return mounted;
|
||||
}
|
||||
|
||||
describe('TableSelector', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(async () => {
|
||||
fetchMock.reset();
|
||||
wrapper = await mountAndWait();
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(TableSelector)).toExist();
|
||||
expect(wrapper.find(DatabaseSelector)).toExist();
|
||||
});
|
||||
|
||||
describe('change database', () => {
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should fetch schemas', async () => {
|
||||
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, { overwriteRoutes: true });
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should fetch schema options', async () => {
|
||||
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
wrapper.update();
|
||||
expect(fetchMock.calls(FETCH_SCHEMAS_ENDPOINT)).toHaveLength(1);
|
||||
|
||||
expect(
|
||||
wrapper.find('[name="select-schema"]').first().props().options,
|
||||
).toEqual([
|
||||
{ value: 'main', label: 'main', title: 'main' },
|
||||
{ value: 'erf', label: 'erf', title: 'erf' },
|
||||
{ value: 'superset', label: 'superset', title: 'superset' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should clear table options', async () => {
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
const props = wrapper.find('[name="async-select-table"]').first().props();
|
||||
expect(props.isDisabled).toBe(true);
|
||||
expect(props.value).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change schema', () => {
|
||||
beforeEach(async () => {
|
||||
fetchMock.get(FETCH_SCHEMAS_ENDPOINT, schemaOptions, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should fetch table', async () => {
|
||||
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, { overwriteRoutes: true });
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-schema"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedSchema);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should fetch table options', async () => {
|
||||
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
act(() => {
|
||||
wrapper.find('[data-test="select-database"]').first().props().onChange({
|
||||
id: 1,
|
||||
database_name: 'main',
|
||||
});
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-schema"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedSchema);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(
|
||||
wrapper.find('[name="select-schema"]').first().props().value[0],
|
||||
).toEqual(selectedSchema);
|
||||
expect(fetchMock.calls(GET_TABLE_NAMES_ENDPOINT)).toHaveLength(1);
|
||||
const { options } = wrapper.find('[name="select-table"]').first().props();
|
||||
expect({ options }).toEqual(tables);
|
||||
});
|
||||
});
|
||||
|
||||
describe('change table', () => {
|
||||
beforeEach(async () => {
|
||||
fetchMock.get(GET_TABLE_NAMES_ENDPOINT, tables, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should change table value', async () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-schema"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedSchema);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-table"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedTable);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(
|
||||
wrapper.find('[name="select-table"]').first().props().value,
|
||||
).toEqual('birth_names');
|
||||
});
|
||||
|
||||
it('should call onTableChange with schema from table object', async () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-schema"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedSchema);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="select-table"]')
|
||||
.first()
|
||||
.props()
|
||||
.onChange(selectedTable);
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(mockedProps.onTableChange.getCall(0).args[0]).toBe('birth_names');
|
||||
expect(mockedProps.onTableChange.getCall(0).args[1]).toBe('main');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTableNamesBySubStr', () => {
|
||||
afterEach(fetchMock.resetHistory);
|
||||
afterAll(fetchMock.reset);
|
||||
|
||||
it('should handle empty', async () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="async-select-table"]')
|
||||
.first()
|
||||
.props()
|
||||
.loadOptions();
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
const props = wrapper.find('[name="async-select-table"]').first().props();
|
||||
expect(props.isDisabled).toBe(true);
|
||||
expect(props.value).toEqual('');
|
||||
});
|
||||
|
||||
it('should handle table name', async () => {
|
||||
wrapper.setProps({ schema: 'main' });
|
||||
fetchMock.get(GET_TABLE_ENDPOINT, tables, {
|
||||
overwriteRoutes: true,
|
||||
});
|
||||
act(() => {
|
||||
wrapper
|
||||
.find('[name="async-select-table"]')
|
||||
.first()
|
||||
.props()
|
||||
.loadOptions();
|
||||
});
|
||||
await waitForComponentToPaint(wrapper);
|
||||
expect(fetchMock.calls(GET_TABLE_ENDPOINT)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,91 +0,0 @@
|
|||
/**
|
||||
* 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 { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||
import { SupersetClient } from '@superset-ui/core';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import TableSelector from '.';
|
||||
|
||||
const SupersetClientGet = jest.spyOn(SupersetClient, 'get');
|
||||
|
||||
const createProps = () => ({
|
||||
dbId: 1,
|
||||
schema: 'test_schema',
|
||||
handleError: jest.fn(),
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
SupersetClientGet.mockImplementation(
|
||||
async () =>
|
||||
({
|
||||
json: {
|
||||
options: [
|
||||
{ label: 'table_a', value: 'table_a' },
|
||||
{ label: 'table_b', value: 'table_b' },
|
||||
],
|
||||
},
|
||||
} as any),
|
||||
);
|
||||
});
|
||||
|
||||
test('renders with default props', async () => {
|
||||
const props = createProps();
|
||||
render(<TableSelector {...props} />);
|
||||
const databaseSelect = screen.getByRole('combobox', {
|
||||
name: 'Select a database',
|
||||
});
|
||||
const schemaSelect = screen.getByRole('combobox', {
|
||||
name: 'Select a database',
|
||||
});
|
||||
const tableSelect = screen.getByRole('combobox', {
|
||||
name: 'Select a table',
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(databaseSelect).toBeInTheDocument();
|
||||
expect(schemaSelect).toBeInTheDocument();
|
||||
expect(tableSelect).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('renders table options', async () => {
|
||||
const props = createProps();
|
||||
render(<TableSelector {...props} />);
|
||||
const tableSelect = screen.getByRole('combobox', {
|
||||
name: 'Select a table',
|
||||
});
|
||||
userEvent.click(tableSelect);
|
||||
expect(
|
||||
await screen.findByRole('option', { name: 'table_a' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByRole('option', { name: 'table_b' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders disabled without schema', async () => {
|
||||
const props = createProps();
|
||||
render(<TableSelector {...props} schema={undefined} />);
|
||||
const tableSelect = screen.getByRole('combobox', {
|
||||
name: 'Select a table',
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(tableSelect).toBeDisabled();
|
||||
});
|
||||
});
|
|
@ -18,49 +18,57 @@
|
|||
*/
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
useMemo,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { styled, SupersetClient, t } from '@superset-ui/core';
|
||||
import { Select } from 'src/components';
|
||||
import { AsyncSelect, CreatableSelect, Select } from 'src/components/Select';
|
||||
|
||||
import { FormLabel } from 'src/components/Form';
|
||||
import Icons from 'src/components/Icons';
|
||||
|
||||
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`
|
||||
${({ theme }) => `
|
||||
.refresh {
|
||||
.fa-refresh {
|
||||
padding-left: 9px;
|
||||
}
|
||||
|
||||
.refresh-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
margin-left: ${theme.gridUnit}px;
|
||||
margin-top: ${theme.gridUnit * 5}px;
|
||||
margin-left: ${({ theme }) => theme.gridUnit}px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding-bottom: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.select {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border-bottom: 1px solid ${theme.colors.secondary.light5};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.colors.secondary.light5};
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.table-length {
|
||||
color: ${theme.colors.grayscale.light1};
|
||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
}
|
||||
|
||||
.select {
|
||||
flex: 1;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const TableLabel = styled.span`
|
||||
|
@ -90,15 +98,7 @@ interface TableSelectorProps {
|
|||
schema?: string;
|
||||
tableName?: string;
|
||||
}) => void;
|
||||
onDbChange?: (
|
||||
db:
|
||||
| {
|
||||
id: number;
|
||||
database_name: string;
|
||||
backend: string;
|
||||
}
|
||||
| undefined,
|
||||
) => void;
|
||||
onDbChange?: (db: any) => void;
|
||||
onSchemaChange?: (arg0?: any) => {};
|
||||
onSchemasLoad?: () => void;
|
||||
onTableChange?: (tableName: string, schema: string) => void;
|
||||
|
@ -110,52 +110,6 @@ interface TableSelectorProps {
|
|||
tableNameSticky?: boolean;
|
||||
}
|
||||
|
||||
interface Table {
|
||||
label: string;
|
||||
value: string;
|
||||
type: string;
|
||||
extra?: {
|
||||
certification?: {
|
||||
certified_by: string;
|
||||
details: string;
|
||||
};
|
||||
warning_markdown?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface TableOption {
|
||||
label: JSX.Element;
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const TableOption = ({ table }: { table: Table }) => {
|
||||
const { label, type, extra } = table;
|
||||
return (
|
||||
<TableLabel title={label}>
|
||||
{type === 'view' ? (
|
||||
<Icons.Eye iconSize="m" />
|
||||
) : (
|
||||
<Icons.Table iconSize="m" />
|
||||
)}
|
||||
{extra?.certification && (
|
||||
<CertifiedIcon
|
||||
certifiedBy={extra.certification.certified_by}
|
||||
details={extra.certification.details}
|
||||
size="l"
|
||||
/>
|
||||
)}
|
||||
{extra?.warning_markdown && (
|
||||
<WarningIconWithTooltip
|
||||
warningMarkdown={extra.warning_markdown}
|
||||
size="l"
|
||||
/>
|
||||
)}
|
||||
{label}
|
||||
</TableLabel>
|
||||
);
|
||||
};
|
||||
|
||||
const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
||||
database,
|
||||
dbId,
|
||||
|
@ -175,187 +129,179 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||
tableName,
|
||||
tableNameSticky = true,
|
||||
}) => {
|
||||
const [currentDbId, setCurrentDbId] = useState<number | undefined>(dbId);
|
||||
const [currentSchema, setCurrentSchema] = useState<string | undefined>(
|
||||
schema,
|
||||
);
|
||||
const [currentTable, setCurrentTable] = useState<TableOption | undefined>();
|
||||
const [refresh, setRefresh] = useState(0);
|
||||
const [previousRefresh, setPreviousRefresh] = useState(0);
|
||||
|
||||
const loadTable = useMemo(
|
||||
() => async (dbId: number, schema: string, tableName: string) => {
|
||||
const endpoint = encodeURI(
|
||||
`/superset/tables/${dbId}/${schema}/${encodeURIComponent(
|
||||
const [currentTableName, setCurrentTableName] = useState<string | undefined>(
|
||||
tableName,
|
||||
)}/false/true`,
|
||||
);
|
||||
const [tableLoading, setTableLoading] = useState(false);
|
||||
const [tableOptions, setTableOptions] = useState([]);
|
||||
|
||||
if (previousRefresh !== refresh) {
|
||||
setPreviousRefresh(refresh);
|
||||
}
|
||||
|
||||
return SupersetClient.get({ endpoint }).then(({ json }) => {
|
||||
const options = json.options as Table[];
|
||||
if (options && options.length > 0) {
|
||||
return options[0];
|
||||
}
|
||||
return null;
|
||||
});
|
||||
},
|
||||
[], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
);
|
||||
|
||||
const loadTables = useMemo(
|
||||
() => async (search: string) => {
|
||||
function fetchTables(
|
||||
databaseId?: number,
|
||||
schema?: string,
|
||||
forceRefresh = false,
|
||||
substr = 'undefined',
|
||||
) {
|
||||
const dbSchema = schema || currentSchema;
|
||||
if (currentDbId && dbSchema) {
|
||||
const actualDbId = databaseId || dbId;
|
||||
if (actualDbId && dbSchema) {
|
||||
const encodedSchema = encodeURIComponent(dbSchema);
|
||||
const encodedSubstr = encodeURIComponent(search || 'undefined');
|
||||
const forceRefresh = refresh !== previousRefresh;
|
||||
const encodedSubstr = encodeURIComponent(substr);
|
||||
setTableLoading(true);
|
||||
setTableOptions([]);
|
||||
const endpoint = encodeURI(
|
||||
`/superset/tables/${currentDbId}/${encodedSchema}/${encodedSubstr}/${forceRefresh}/`,
|
||||
`/superset/tables/${actualDbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
|
||||
);
|
||||
|
||||
if (previousRefresh !== refresh) {
|
||||
setPreviousRefresh(refresh);
|
||||
}
|
||||
|
||||
return SupersetClient.get({ endpoint }).then(({ json }) => {
|
||||
const options = json.options
|
||||
.map((table: Table) => ({
|
||||
value: table.value,
|
||||
label: <TableOption table={table} />,
|
||||
text: table.label,
|
||||
}))
|
||||
.sort((a: { text: string }, b: { text: string }) =>
|
||||
a.text.localeCompare(b.text),
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
data: options,
|
||||
totalCount: options.length,
|
||||
};
|
||||
})
|
||||
.catch(() => {
|
||||
setTableLoading(false);
|
||||
setTableOptions([]);
|
||||
handleError(t('Error while fetching table list'));
|
||||
});
|
||||
}
|
||||
return { data: [], totalCount: 0 };
|
||||
},
|
||||
// We are using the refresh state to re-trigger the query
|
||||
// previousRefresh should be out of dependencies array
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[currentDbId, currentSchema, onTablesLoad, schema, refresh],
|
||||
);
|
||||
setTableLoading(false);
|
||||
setTableOptions([]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchTable() {
|
||||
if (schema && tableName) {
|
||||
const table = await loadTable(dbId, schema, tableName);
|
||||
if (table) {
|
||||
setCurrentTable({
|
||||
label: <TableOption table={table} />,
|
||||
text: table.label,
|
||||
value: table.value,
|
||||
});
|
||||
if (dbId && schema) {
|
||||
fetchTables();
|
||||
}
|
||||
}
|
||||
}
|
||||
fetchTable();
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [dbId, schema]);
|
||||
|
||||
function onSelectionChange({
|
||||
dbId,
|
||||
schema,
|
||||
table,
|
||||
tableName,
|
||||
}: {
|
||||
dbId: number;
|
||||
schema?: string;
|
||||
table?: TableOption;
|
||||
tableName?: string;
|
||||
}) {
|
||||
setCurrentTable(table);
|
||||
setCurrentDbId(dbId);
|
||||
setCurrentTableName(tableName);
|
||||
setCurrentSchema(schema);
|
||||
if (onUpdate) {
|
||||
onUpdate({ dbId, schema, tableName: table?.value });
|
||||
onUpdate({ dbId, schema, tableName });
|
||||
}
|
||||
}
|
||||
|
||||
function changeTable(table: TableOption) {
|
||||
if (!table) {
|
||||
setCurrentTable(undefined);
|
||||
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 tableOptTableName = table.value;
|
||||
if (currentDbId && tableNameSticky) {
|
||||
const schemaName = tableOpt.schema;
|
||||
const tableOptTableName = tableOpt.value;
|
||||
if (tableNameSticky) {
|
||||
onSelectionChange({
|
||||
dbId: currentDbId,
|
||||
schema: currentSchema,
|
||||
table,
|
||||
dbId,
|
||||
schema: schemaName,
|
||||
tableName: tableOptTableName,
|
||||
});
|
||||
}
|
||||
if (onTableChange && currentSchema) {
|
||||
onTableChange(tableOptTableName, currentSchema);
|
||||
if (onTableChange) {
|
||||
onTableChange(tableOptTableName, schemaName);
|
||||
}
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
function changeSchema(schemaOpt: any, force = false) {
|
||||
const value = schemaOpt ? schemaOpt.value : null;
|
||||
if (onSchemaChange) {
|
||||
onSchemaChange(currentSchema);
|
||||
onSchemaChange(value);
|
||||
}
|
||||
if (currentDbId && currentSchema) {
|
||||
onSelectionChange({
|
||||
dbId: currentDbId,
|
||||
schema: currentSchema,
|
||||
table: currentTable,
|
||||
dbId,
|
||||
schema: value,
|
||||
tableName: undefined,
|
||||
});
|
||||
fetchTables(dbId, currentSchema, force);
|
||||
}
|
||||
setRefresh(refresh + 1);
|
||||
|
||||
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">{refreshBtn}</span>
|
||||
<span className="refresh-col">{refreshBtn}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const internalDbChange = (
|
||||
db:
|
||||
| {
|
||||
id: number;
|
||||
database_name: string;
|
||||
backend: string;
|
||||
}
|
||||
| undefined,
|
||||
) => {
|
||||
setCurrentDbId(db?.id);
|
||||
if (onDbChange) {
|
||||
onDbChange(db);
|
||||
}
|
||||
};
|
||||
|
||||
const internalSchemaChange = (schema?: string) => {
|
||||
setCurrentSchema(schema);
|
||||
if (onSchemaChange) {
|
||||
onSchemaChange(schema);
|
||||
}
|
||||
};
|
||||
|
||||
function renderDatabaseSelector() {
|
||||
return (
|
||||
<DatabaseSelector
|
||||
db={database}
|
||||
dbId={dbId}
|
||||
formMode={formMode}
|
||||
getDbList={getDbList}
|
||||
getTableList={fetchTables}
|
||||
handleError={handleError}
|
||||
onUpdate={onSelectionChange}
|
||||
onDbChange={readOnly ? undefined : internalDbChange}
|
||||
onSchemaChange={readOnly ? undefined : internalSchemaChange}
|
||||
onDbChange={readOnly ? undefined : onDbChange}
|
||||
onSchemaChange={readOnly ? undefined : onSchemaChange}
|
||||
onSchemasLoad={onSchemasLoad}
|
||||
schema={currentSchema}
|
||||
sqlLabMode={sqlLabMode}
|
||||
|
@ -365,54 +311,96 @@ const TableSelector: FunctionComponent<TableSelectorProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
const handleFilterOption = useMemo(
|
||||
() => (search: string, option: TableOption) => {
|
||||
const searchValue = search.trim().toLowerCase();
|
||||
const { text } = option;
|
||||
return text.toLowerCase().includes(searchValue);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
function renderTableSelect() {
|
||||
const disabled =
|
||||
(currentSchema && !formMode && readOnly) ||
|
||||
(!currentSchema && !database?.allow_multi_schema_metadata_fetch);
|
||||
|
||||
const header = sqlLabMode ? (
|
||||
<FormLabel>{t('See table schema')}</FormLabel>
|
||||
) : (
|
||||
<FormLabel>{t('Table')}</FormLabel>
|
||||
);
|
||||
|
||||
const select = (
|
||||
const options = tableOptions;
|
||||
let select = null;
|
||||
if (currentSchema && !formMode) {
|
||||
// dataset editor
|
||||
select = (
|
||||
<Select
|
||||
ariaLabel={t('Select a table')}
|
||||
disabled={disabled}
|
||||
filterOption={handleFilterOption}
|
||||
header={header}
|
||||
name="select-table"
|
||||
isLoading={tableLoading}
|
||||
ignoreAccents={false}
|
||||
placeholder={t('Select table or type table name')}
|
||||
autosize={false}
|
||||
onChange={changeTable}
|
||||
options={loadTables}
|
||||
placeholder={t('Select a table')}
|
||||
value={currentTable}
|
||||
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={onRefresh}
|
||||
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()}
|
||||
{sqlLabMode && !formMode && <div className="divider" />}
|
||||
{!formMode && <div className="divider" />}
|
||||
{sqlLabMode && renderSeeTableLabel()}
|
||||
{formMode && <FieldTitle>{t('Table')}</FieldTitle>}
|
||||
{renderTableSelect()}
|
||||
</TableSelectorWrapper>
|
||||
);
|
||||
|
|
|
@ -18,17 +18,16 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { useTheme, SafeMarkdown } from '@superset-ui/core';
|
||||
import Icons, { IconType } from 'src/components/Icons';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
|
||||
export interface WarningIconWithTooltipProps {
|
||||
warningMarkdown: string;
|
||||
size?: IconType['iconSize'];
|
||||
size?: number;
|
||||
}
|
||||
|
||||
function WarningIconWithTooltip({
|
||||
warningMarkdown,
|
||||
size,
|
||||
}: WarningIconWithTooltipProps) {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
|
@ -38,7 +37,6 @@ function WarningIconWithTooltip({
|
|||
>
|
||||
<Icons.AlertSolid
|
||||
iconColor={theme.colors.alert.base}
|
||||
iconSize={size}
|
||||
css={{ marginRight: theme.gridUnit * 2 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
|
|
@ -775,14 +775,12 @@ class DatasourceEditor extends React.PureComponent {
|
|||
<div>
|
||||
{this.state.isSqla && (
|
||||
<>
|
||||
<Col xs={24} md={12}>
|
||||
<Field
|
||||
fieldKey="databaseSelector"
|
||||
label={t('virtual')}
|
||||
control={
|
||||
<div css={{ marginTop: 8 }}>
|
||||
<DatabaseSelector
|
||||
db={datasource?.database}
|
||||
dbId={datasource.database.id}
|
||||
schema={datasource.schema}
|
||||
onSchemaChange={schema =>
|
||||
this.state.isEditMode &&
|
||||
|
@ -796,10 +794,8 @@ class DatasourceEditor extends React.PureComponent {
|
|||
handleError={this.props.addDangerToast}
|
||||
readOnly={!this.state.isEditMode}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div css={{ width: 'calc(100% - 34px)', marginTop: -16 }}>
|
||||
<Field
|
||||
fieldKey="table_name"
|
||||
label={t('Dataset name')}
|
||||
|
@ -814,8 +810,6 @@ class DatasourceEditor extends React.PureComponent {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Field
|
||||
fieldKey="sql"
|
||||
label={t('SQL')}
|
||||
|
@ -859,10 +853,8 @@ class DatasourceEditor extends React.PureComponent {
|
|||
fieldKey="tableSelector"
|
||||
label={t('Physical')}
|
||||
control={
|
||||
<div css={{ marginTop: 8 }}>
|
||||
<TableSelector
|
||||
clearable={false}
|
||||
database={datasource.database}
|
||||
dbId={datasource.database.id}
|
||||
handleError={this.props.addDangerToast}
|
||||
schema={datasource.schema}
|
||||
|
@ -877,10 +869,7 @@ class DatasourceEditor extends React.PureComponent {
|
|||
onDbChange={
|
||||
this.state.isEditMode
|
||||
? database =>
|
||||
this.onDatasourcePropChange(
|
||||
'database',
|
||||
database,
|
||||
)
|
||||
this.onDatasourcePropChange('database', database)
|
||||
: undefined
|
||||
}
|
||||
onTableChange={
|
||||
|
@ -891,7 +880,6 @@ class DatasourceEditor extends React.PureComponent {
|
|||
}
|
||||
readOnly={!this.state.isEditMode}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
description={t(
|
||||
'The pointer to a physical table (or view). Keep in mind that the chart is ' +
|
||||
|
|
|
@ -227,7 +227,10 @@ class DatasourceControl extends React.PureComponent {
|
|||
</Tooltip>
|
||||
)}
|
||||
{extra?.warning_markdown && (
|
||||
<WarningIconWithTooltip warningMarkdown={extra.warning_markdown} />
|
||||
<WarningIconWithTooltip
|
||||
warningMarkdown={extra.warning_markdown}
|
||||
size={30}
|
||||
/>
|
||||
)}
|
||||
<Dropdown
|
||||
overlay={datasourceMenu}
|
||||
|
|
|
@ -243,13 +243,11 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
|
|||
<CertifiedIcon
|
||||
certifiedBy={parsedExtra.certification.certified_by}
|
||||
details={parsedExtra.certification.details}
|
||||
size="l"
|
||||
/>
|
||||
)}
|
||||
{parsedExtra?.warning_markdown && (
|
||||
<WarningIconWithTooltip
|
||||
warningMarkdown={parsedExtra.warning_markdown}
|
||||
size="l"
|
||||
/>
|
||||
)}
|
||||
{titleLink}
|
||||
|
|
|
@ -160,7 +160,7 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
"url",
|
||||
"extra",
|
||||
]
|
||||
show_columns = show_select_columns + ["columns.type_generic", "database.backend"]
|
||||
show_columns = show_select_columns + ["columns.type_generic"]
|
||||
add_model_schema = DatasetPostSchema()
|
||||
edit_model_schema = DatasetPutSchema()
|
||||
add_columns = ["database", "schema", "table_name", "owners"]
|
||||
|
|
|
@ -1058,14 +1058,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
@event_logger.log_this
|
||||
@expose("/tables/<int:db_id>/<schema>/<substr>/")
|
||||
@expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/")
|
||||
@expose("/tables/<int:db_id>/<schema>/<substr>/<force_refresh>/<exact_match>")
|
||||
def tables( # pylint: disable=too-many-locals,no-self-use,too-many-arguments
|
||||
self,
|
||||
db_id: int,
|
||||
schema: str,
|
||||
substr: str,
|
||||
force_refresh: str = "false",
|
||||
exact_match: str = "false",
|
||||
def tables( # pylint: disable=too-many-locals,no-self-use
|
||||
self, db_id: int, schema: str, substr: str, force_refresh: str = "false"
|
||||
) -> FlaskResponse:
|
||||
"""Endpoint to fetch the list of tables for given database"""
|
||||
# Guarantees database filtering by security access
|
||||
|
@ -1078,7 +1072,6 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
return json_error_response("Not found", 404)
|
||||
|
||||
force_refresh_parsed = force_refresh.lower() == "true"
|
||||
exact_match_parsed = exact_match.lower() == "true"
|
||||
schema_parsed = utils.parse_js_uri_path_item(schema, eval_undefined=True)
|
||||
substr_parsed = utils.parse_js_uri_path_item(substr, eval_undefined=True)
|
||||
|
||||
|
@ -1120,15 +1113,9 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
|
|||
ds_name.table if schema_parsed else f"{ds_name.schema}.{ds_name.table}"
|
||||
)
|
||||
|
||||
def is_match(src: str, target: utils.DatasourceName) -> bool:
|
||||
target_label = get_datasource_label(target)
|
||||
if exact_match_parsed:
|
||||
return src == target_label
|
||||
return src in target_label
|
||||
|
||||
if substr_parsed:
|
||||
tables = [tn for tn in tables if is_match(substr_parsed, tn)]
|
||||
views = [vn for vn in views if is_match(substr_parsed, vn)]
|
||||
tables = [tn for tn in tables if substr_parsed in get_datasource_label(tn)]
|
||||
views = [vn for vn in views if substr_parsed in get_datasource_label(vn)]
|
||||
|
||||
if not schema_parsed and database.default_schemas:
|
||||
user_schemas = (
|
||||
|
|
|
@ -222,7 +222,6 @@ class TestDatasetApi(SupersetTestCase):
|
|||
Dataset API: Test get dataset item
|
||||
"""
|
||||
table = self.get_energy_usage_dataset()
|
||||
main_db = get_main_database()
|
||||
self.login(username="admin")
|
||||
uri = f"api/v1/dataset/{table.id}"
|
||||
rv = self.get_assert_metric(uri, "get")
|
||||
|
@ -230,11 +229,7 @@ class TestDatasetApi(SupersetTestCase):
|
|||
response = json.loads(rv.data.decode("utf-8"))
|
||||
expected_result = {
|
||||
"cache_timeout": None,
|
||||
"database": {
|
||||
"backend": main_db.backend,
|
||||
"database_name": "examples",
|
||||
"id": 1,
|
||||
},
|
||||
"database": {"database_name": "examples", "id": 1},
|
||||
"default_endpoint": None,
|
||||
"description": "Energy consumption",
|
||||
"extra": None,
|
||||
|
@ -249,7 +244,6 @@ class TestDatasetApi(SupersetTestCase):
|
|||
"table_name": "energy_usage",
|
||||
"template_params": None,
|
||||
}
|
||||
if response["result"]["database"]["backend"] not in ("presto", "hive"):
|
||||
assert {
|
||||
k: v for k, v in response["result"].items() if k in expected_result
|
||||
} == expected_result
|
||||
|
|
Loading…
Reference in New Issue