Compare commits

...

15 Commits

Author SHA1 Message Date
Ville Brofeldt
6cc5b5b616 fix(explore): make clicked dnd filters unique (#16700)
(cherry picked from commit 1d890f8913)
2021-09-14 08:51:15 -07:00
Ville Brofeldt
5aa13ce650 fix(dnd): make clicked dnd metrics unique (#16632)
(cherry picked from commit 9dfa33fedf)
2021-09-08 11:31:24 -07:00
AAfghahi
e7aac3ff21 fix: select database fix (#16472)
* select database fix

* made a backend change

(cherry picked from commit e2469162fa)
2021-08-31 09:17:02 -07:00
AAfghahi
f8212ad3df fix: queryEditor bug (#16452)
* queryEditor bug

* update tests

Co-authored-by: Elizabeth Thompson <eschutho@gmail.com>
(cherry picked from commit ee2eccdb67)
2021-08-31 09:16:38 -07:00
Erik Ritter
9068dd3cb4 Revert "chore: Changes the DatabaseSelector to use the new Select component (#16334)" (#16478)
This reverts commit c768941f2f.

(cherry picked from commit 8adc31d14c)
2021-08-31 09:16:21 -07:00
Ville Brofeldt
31ec5b8063 fix(datasets): add support for removing owners (#16461)
* fix(datasets): add support for removing owners

* default to current user

(cherry picked from commit c5a5cf7db9)
2021-08-31 09:16:00 -07:00
Yongjie Zhao
961b9da4f4 fix: prevent page crash when chart can't render (#16464)
(cherry picked from commit 577ede4b12)
2021-08-26 12:48:37 -07:00
Grace Guo
f6bb1cdd9f fix: setupPlugin in chart list page (#16413)
* fix: setupPlugin in chart list page

* fix the order of setupPlugins call

* Fix jest test on loading geojson

* add jest changes

* fix unit tests

Co-authored-by: Jesse Yang <jesse.yang@airbnb.com>
(cherry picked from commit 08b8aa277f)
2021-08-26 12:47:48 -07:00
Ville Brofeldt
53dea71832 fix(native-filters): handle null values in value filter (#16460)
* fix(native-filters): handle null values in value filter

* lint

(cherry picked from commit 1c71eda70f)
2021-08-26 12:47:28 -07:00
Steven Uray
4fce06e7a3 Revert "Cherrying in content from 16210"
This reverts commit 401cb603d8.
2021-08-25 15:54:56 -07:00
Elizabeth Thompson
79cfad10cf Revert "fix: reverting Dataset names (#16243)"
This reverts commit 4119bb9c1e.

(cherry picked from commit 3feb9d3ff9)
2021-08-24 20:04:54 -07:00
Steven Uray
5bc6ce8325 Cherrying in content from 16242 2021-08-24 20:04:16 -07:00
Steven Uray
401cb603d8 Cherrying in content from 16210 2021-08-24 20:03:10 -07:00
Steven Uray
7d9cb04836 Cherrying in content from 16206 2021-08-24 20:01:56 -07:00
AAfghahi
a235f5b28e feat: Changing Dataset names (#16199)
* added google alert

* changing Dataset Names

(cherry picked from commit 6c304b83a9)
2021-08-24 19:40:28 -07:00
59 changed files with 1013 additions and 794 deletions

View File

@@ -66,7 +66,7 @@ Navigate to **Data ‣ Datasets** and select the **+ Dataset** button in the top
A modal window should pop up in front of you. Select your **Database**,
**Schema**, and **Table** using the drop downs that appear. In the following example,
we register the **cleaned_sales_data** table from the **examples** database.
we register the **Vehicle Sales** table from the **examples** database.
<img src="/images/tutorial_09_add_new_table.png" />

View File

@@ -19,8 +19,8 @@
module.exports = {
testRegex: '(\\/spec|\\/src)\\/.*(_spec|\\.test)\\.(j|t)sx?$',
moduleNameMapper: {
'\\.(css|less)$': '<rootDir>/spec/__mocks__/styleMock.js',
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/spec/__mocks__/fileMock.js',
'\\.(css|less|geojson)$': '<rootDir>/spec/__mocks__/mockExportObject.js',
'\\.(gif|ttf|eot|png|jpg)$': '<rootDir>/spec/__mocks__/mockExportString.js',
'\\.svg$': '<rootDir>/spec/__mocks__/svgrMock.tsx',
'^src/(.*)$': '<rootDir>/src/$1',
'^spec/(.*)$': '<rootDir>/spec/$1',

View File

@@ -23,3 +23,5 @@ import { configure as configureTestingLibrary } from '@testing-library/react';
configureTestingLibrary({
testIdAttribute: 'data-test',
});
document.body.innerHTML = '<div id="app" data-bootstrap="{}"></div>';

View File

@@ -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();

View File

@@ -399,7 +399,7 @@ describe('async actions', () => {
let isFeatureEnabledMock;
beforeAll(() => {
beforeEach(() => {
isFeatureEnabledMock = jest
.spyOn(featureFlags, 'isFeatureEnabled')
.mockImplementation(
@@ -407,7 +407,7 @@ describe('async actions', () => {
);
});
afterAll(() => {
afterEach(() => {
isFeatureEnabledMock.mockRestore();
});
@@ -612,9 +612,29 @@ describe('async actions', () => {
});
describe('queryEditorSetSql', () => {
it('updates the tab state in the backend', () => {
expect.assertions(2);
describe('with backend persistence flag on', () => {
it('does not update the tab state in the backend', () => {
expect.assertions(2);
const sql = 'SELECT * ';
const store = mockStore({});
return store
.dispatch(actions.queryEditorSetSql(queryEditor, sql))
.then(() => {
expect(store.getActions()).toHaveLength(0);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
});
});
});
describe('with backend persistence flag off', () => {
it('updates the tab state in the backend', () => {
const backendPersistenceOffMock = jest
.spyOn(featureFlags, 'isFeatureEnabled')
.mockImplementation(
feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'),
);
const sql = 'SELECT * ';
const store = mockStore({});
const expectedActions = [
@@ -624,12 +644,12 @@ describe('async actions', () => {
sql,
},
];
return store
.dispatch(actions.queryEditorSetSql(queryEditor, sql))
.then(() => {
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(1);
});
store.dispatch(actions.queryEditorSetSql(queryEditor, sql));
expect(store.getActions()).toEqual(expectedActions);
expect(fetchMock.calls(updateTabStateEndpoint)).toHaveLength(0);
backendPersistenceOffMock.mockRestore();
});
});

View File

@@ -898,16 +898,11 @@ export function updateSavedQuery(query) {
export function queryEditorSetSql(queryEditor, sql) {
return function (dispatch) {
const sync = isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)
? SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { sql, latest_query_id: queryEditor.latestQueryId },
})
: Promise.resolve();
return sync
.then(() => dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql }))
.catch(() =>
if (isFeatureEnabled(FeatureFlag.SQLLAB_BACKEND_PERSISTENCE)) {
return SupersetClient.put({
endpoint: encodeURI(`/tabstateview/${queryEditor.id}`),
postPayload: { sql, latest_query_id: queryEditor.latestQueryId },
}).catch(() =>
dispatch(
addDangerToast(
t(
@@ -918,6 +913,8 @@ export function queryEditorSetSql(queryEditor, sql) {
),
),
);
}
return dispatch({ type: QUERY_EDITOR_SET_SQL, queryEditor, sql });
};
}

View File

@@ -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>
);

View File

@@ -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();
});

View File

@@ -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 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 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;
`;
type DatabaseValue = { label: string; value: number };
const DatabaseSelectorWrapper = styled.div`
.fa-refresh {
padding-left: 9px;
}
type SchemaValue = { label: string; value: string };
.refresh-col {
display: flex;
align-items: center;
width: 30px;
margin-left: ${({ theme }) => theme.gridUnit}px;
}
.section {
padding-bottom: 5px;
display: flex;
flex-direction: row;
}
.select {
flex-grow: 1;
}
`;
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,189 +97,193 @@ 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;
}> => {
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() {
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(
<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>
);

View File

@@ -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);
}
setLoaded(true);
}
importIcon();
return () => {
cancelled = true;
};
}, [fileName, ImportedSVG]);
return (

View File

@@ -25,6 +25,7 @@ import OmniContainer from './index';
jest.mock('src/featureFlags', () => ({
isFeatureEnabled: jest.fn(),
FeatureFlag: { OMNIBAR: 'OMNIBAR' },
initFeatureFlags: jest.fn(),
}));
test('Do not open Omnibar with the featureflag disabled', () => {

View File

@@ -47,7 +47,6 @@ type PickedSelectProps = Pick<
AntdSelectAllProps,
| 'allowClear'
| 'autoFocus'
| 'value'
| 'disabled'
| 'filterOption'
| 'notFoundContent'
@@ -87,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;
@@ -187,7 +189,6 @@ const Select = ({
: 'multiple';
useEffect(() => {
fetchedQueries.current.clear();
setSelectOptions(
options && Array.isArray(options) ? options : EMPTY_OPTIONS,
);
@@ -365,45 +366,34 @@ const Select = ({
[options],
);
const handleOnSearch = useMemo(
() =>
debounce((search: string) => {
const searchValue = search.trim();
// enables option creation
if (allowNewOptions && isSingleMode) {
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 (
searchValue &&
firstOption === searchedValue &&
!initialOptions.find(o => o.value === searchedValue)
) {
selectOptions.shift();
setSelectOptions(selectOptions);
}
if (searchValue && !hasOption(searchValue, selectOptions)) {
const newOption = {
label: searchValue,
value: searchValue,
};
// adds a custom option
const newOptions = [...selectOptions, newOption];
setSelectOptions(newOptions);
setSelectValue(searchValue);
}
}
setSearchedValue(searchValue);
}, DEBOUNCE_TIMEOUT),
[
allowNewOptions,
initialOptions,
isSingleMode,
searchedValue,
selectOptions,
],
);
const handleOnSearch = debounce((search: string) => {
const searchValue = search.trim();
// enables option creation
if (allowNewOptions && isSingleMode) {
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 (
searchValue &&
firstOption === searchedValue &&
!initialOptions.find(o => o.value === searchedValue)
) {
selectOptions.shift();
setSelectOptions(selectOptions);
}
if (searchValue && !hasOption(searchValue, selectOptions)) {
const newOption = {
label: searchValue,
value: searchValue,
};
// adds a custom option
const newOptions = [...selectOptions, newOption];
setSelectOptions(newOptions);
setSelectValue(searchValue);
}
}
setSearchedValue(searchValue);
}, DEBOUNCE_TIMEOUT);
const handlePagination = (e: UIEvent<HTMLElement>) => {
const vScroll = e.currentTarget;
@@ -496,6 +486,7 @@ const Select = ({
<StyledContainer>
{header}
<StyledSelect
hasHeader={!!header}
aria-label={ariaLabel || name}
dropdownRender={dropdownRender}
filterOption={handleFilterOption}

View File

@@ -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);
});
});
});

View File

@@ -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();
});
});

View File

@@ -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 {
display: flex;
align-items: center;
width: 30px;
margin-left: ${theme.gridUnit}px;
margin-top: ${theme.gridUnit * 5}px;
}
.fa-refresh {
padding-left: 9px;
}
.section {
display: flex;
flex-direction: row;
align-items: center;
}
.refresh-col {
display: flex;
align-items: center;
width: 30px;
margin-left: ${({ theme }) => theme.gridUnit}px;
}
.divider {
border-bottom: 1px solid ${theme.colors.secondary.light5};
margin: 15px 0;
}
.section {
padding-bottom: 5px;
display: flex;
flex-direction: row;
}
.table-length {
color: ${theme.colors.grayscale.light1};
}
.select {
flex-grow: 1;
}
.select {
flex: 1;
}
`}
.divider {
border-bottom: 1px solid ${({ theme }) => theme.colors.secondary.light5};
margin: 15px 0;
}
.table-length {
color: ${({ theme }) => theme.colors.grayscale.light1};
}
`;
const TableLabel = styled.span`
@@ -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(
tableName,
)}/false/true`,
);
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 [currentTableName, setCurrentTableName] = useState<string | undefined>(
tableName,
);
const [tableLoading, setTableLoading] = useState(false);
const [tableOptions, setTableOptions] = useState([]);
const loadTables = useMemo(
() => async (search: string) => {
const dbSchema = schema || currentSchema;
if (currentDbId && dbSchema) {
const encodedSchema = encodeURIComponent(dbSchema);
const encodedSubstr = encodeURIComponent(search || 'undefined');
const forceRefresh = refresh !== previousRefresh;
const endpoint = encodeURI(
`/superset/tables/${currentDbId}/${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),
);
function fetchTables(
databaseId?: number,
schema?: string,
forceRefresh = false,
substr = 'undefined',
) {
const dbSchema = schema || currentSchema;
const actualDbId = databaseId || dbId;
if (actualDbId && dbSchema) {
const encodedSchema = encodeURIComponent(dbSchema);
const encodedSubstr = encodeURIComponent(substr);
setTableLoading(true);
setTableOptions([]);
const endpoint = encodeURI(
`/superset/tables/${actualDbId}/${encodedSchema}/${encodedSubstr}/${!!forceRefresh}/`,
);
return SupersetClient.get({ endpoint })
.then(({ json }) => {
const options = json.options.map((o: any) => ({
value: o.value,
schema: o.schema,
label: o.label,
title: o.title,
type: o.type,
extra: o?.extra,
}));
setTableLoading(false);
setTableOptions(options);
if (onTablesLoad) {
onTablesLoad(json.options);
}
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,
});
}
setRefresh(refresh + 1);
onSelectionChange({
dbId,
schema: value,
tableName: undefined,
});
fetchTables(dbId, currentSchema, force);
}
function renderTableOption(option: any) {
return (
<TableLabel title={option.label}>
<small className="text-muted">
<i className={`fa fa-${option.type === 'view' ? 'eye' : 'table'}`} />
</small>
{option.extra?.certification && (
<CertifiedIcon
certifiedBy={option.extra.certification.certified_by}
details={option.extra.certification.details}
size={20}
/>
)}
{option.extra?.warning_markdown && (
<WarningIconWithTooltip
warningMarkdown={option.extra.warning_markdown}
size={20}
/>
)}
{option.label}
</TableLabel>
);
}
function renderSelectRow(select: ReactNode, refreshBtn: ReactNode) {
return (
<div className="section">
<span className="select">{select}</span>
<span className="refresh">{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 = (
<Select
ariaLabel={t('Select a table')}
disabled={disabled}
filterOption={handleFilterOption}
header={header}
name="select-table"
onChange={changeTable}
options={loadTables}
placeholder={t('Select a table')}
value={currentTable}
/>
);
const options = tableOptions;
let select = null;
if (currentSchema && !formMode) {
// dataset editor
select = (
<Select
name="select-table"
isLoading={tableLoading}
ignoreAccents={false}
placeholder={t('Select table or type table name')}
autosize={false}
onChange={changeTable}
options={options}
// @ts-ignore
value={currentTableName}
optionRenderer={renderTableOption}
valueRenderer={renderTableOption}
isDisabled={readOnly}
/>
);
} else if (formMode) {
select = (
<CreatableSelect
name="select-table"
isLoading={tableLoading}
ignoreAccents={false}
placeholder={t('Select table or type table name')}
autosize={false}
onChange={changeTable}
options={options}
// @ts-ignore
value={currentTableName}
optionRenderer={renderTableOption}
/>
);
} else {
// sql lab
let tableSelectPlaceholder;
let tableSelectDisabled = false;
if (database && database.allow_multi_schema_metadata_fetch) {
tableSelectPlaceholder = t('Type to search ...');
} else {
tableSelectPlaceholder = t('Select table ');
tableSelectDisabled = true;
}
select = (
<AsyncSelect
name="async-select-table"
placeholder={tableSelectPlaceholder}
isDisabled={tableSelectDisabled}
autosize={false}
onChange={changeTable}
// @ts-ignore
value={currentTableName}
loadOptions={getTableNamesBySubStr}
optionRenderer={renderTableOption}
/>
);
}
const refresh = !formMode && !readOnly && (
<RefreshLabel
onClick={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>
);

View File

@@ -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>

View File

@@ -775,47 +775,41 @@ 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}
schema={datasource.schema}
onSchemaChange={schema =>
this.state.isEditMode &&
this.onDatasourcePropChange('schema', schema)
}
onDbChange={database =>
this.state.isEditMode &&
this.onDatasourcePropChange('database', database)
}
formMode={false}
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')}
control={
<TextControl
controlId="table_name"
onChange={table => {
this.onDatasourcePropChange('table_name', table);
}}
placeholder={t('Dataset name')}
disabled={!this.state.isEditMode}
/>
<Field
fieldKey="databaseSelector"
label={t('virtual')}
control={
<DatabaseSelector
dbId={datasource.database.id}
schema={datasource.schema}
onSchemaChange={schema =>
this.state.isEditMode &&
this.onDatasourcePropChange('schema', schema)
}
onDbChange={database =>
this.state.isEditMode &&
this.onDatasourcePropChange('database', database)
}
formMode={false}
handleError={this.props.addDangerToast}
readOnly={!this.state.isEditMode}
/>
</div>
</Col>
}
/>
<Field
fieldKey="table_name"
label={t('Dataset name')}
control={
<TextControl
controlId="table_name"
onChange={table => {
this.onDatasourcePropChange('table_name', table);
}}
placeholder={t('Dataset name')}
disabled={!this.state.isEditMode}
/>
}
/>
<Field
fieldKey="sql"
label={t('SQL')}
@@ -859,39 +853,33 @@ 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}
sqlLabMode={false}
tableName={datasource.table_name}
onSchemaChange={
this.state.isEditMode
? schema =>
this.onDatasourcePropChange('schema', schema)
: undefined
}
onDbChange={
this.state.isEditMode
? database =>
this.onDatasourcePropChange(
'database',
database,
)
: undefined
}
onTableChange={
this.state.isEditMode
? table =>
this.onDatasourcePropChange('table_name', table)
: undefined
}
readOnly={!this.state.isEditMode}
/>
</div>
<TableSelector
clearable={false}
dbId={datasource.database.id}
handleError={this.props.addDangerToast}
schema={datasource.schema}
sqlLabMode={false}
tableName={datasource.table_name}
onSchemaChange={
this.state.isEditMode
? schema =>
this.onDatasourcePropChange('schema', schema)
: undefined
}
onDbChange={
this.state.isEditMode
? database =>
this.onDatasourcePropChange('database', database)
: undefined
}
onTableChange={
this.state.isEditMode
? table =>
this.onDatasourcePropChange('table_name', table)
: undefined
}
readOnly={!this.state.isEditMode}
/>
}
description={t(
'The pointer to a physical table (or view). Keep in mind that the chart is ' +

View File

@@ -224,7 +224,7 @@ export const DataTablesPane = ({
}, [queryFormData.adhoc_filters, queryFormData.datasource]);
useEffect(() => {
if (queriesResponse) {
if (queriesResponse && chartStatus === 'success') {
const { colnames } = queriesResponse[0];
setColumnNames([...colnames]);
}

View File

@@ -25,3 +25,13 @@ export interface DatasourcePanelDndItem {
value: DndItemValue;
type: DndItemType;
}
export function isDatasourcePanelDndItem(
item: any,
): item is DatasourcePanelDndItem {
return item?.value && item?.type;
}
export function isSavedMetric(item: any): item is Metric {
return item?.metric_name;
}

View File

@@ -48,7 +48,7 @@ const createProps = () => ({
cache_timeout: null,
changed_on: '2021-03-19T16:30:56.750230',
changed_on_humanized: '3 days ago',
datasource: 'FCC 2018 Survey',
datasource: 'FCC Survey Results',
description: null,
description_markeddown: '',
edit_url: '/chart/edit/318',

View File

@@ -29,7 +29,7 @@ const createProps = () => ({
cache_timeout: null,
changed_on: '2021-03-19T16:30:56.750230',
changed_on_humanized: '7 days ago',
datasource: 'FCC 2018 Survey',
datasource: 'FCC Survey Results',
description: null,
description_markeddown: '',
edit_url: '/chart/edit/318',

View File

@@ -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}

View File

@@ -46,9 +46,11 @@ import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetr
import {
DatasourcePanelDndItem,
DndItemValue,
isSavedMetric,
} from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType';
const EMPTY_OBJECT = {};
const DND_ACCEPTED_TYPES = [
DndItemType.Column,
DndItemType.Metric,
@@ -70,7 +72,9 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
);
const [partitionColumn, setPartitionColumn] = useState(undefined);
const [newFilterPopoverVisible, setNewFilterPopoverVisible] = useState(false);
const [droppedItem, setDroppedItem] = useState<DndItemValue | null>(null);
const [droppedItem, setDroppedItem] = useState<
DndItemValue | typeof EMPTY_OBJECT
>({});
const optionsForSelect = (
columns: ColumnMeta[],
@@ -334,12 +338,12 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
);
const handleClickGhostButton = useCallback(() => {
setDroppedItem(null);
setDroppedItem({});
togglePopover(true);
}, [togglePopover]);
const adhocFilter = useMemo(() => {
if (droppedItem?.metric_name) {
if (isSavedMetric(droppedItem)) {
return new AdhocFilter({
expressionType: EXPRESSION_TYPES.SQL,
clause: CLAUSES.HAVING,

View File

@@ -32,8 +32,11 @@ import { usePrevious } from 'src/common/hooks/usePrevious';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
import AdhocMetricPopoverTrigger from 'src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger';
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
import {
DatasourcePanelDndItem,
isDatasourcePanelDndItem,
} from 'src/explore/components/DatasourcePanel/types';
import { OptionValueType } from 'src/explore/components/controls/DndColumnSelectControl/types';
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
@@ -133,9 +136,9 @@ export const DndMetricSelect = (props: any) => {
const [value, setValue] = useState<(AdhocMetric | Metric | string)[]>(
coerceAdhocMetrics(props.value),
);
const [droppedItem, setDroppedItem] = useState<DatasourcePanelDndItem | null>(
null,
);
const [droppedItem, setDroppedItem] = useState<
DatasourcePanelDndItem | typeof EMPTY_OBJECT
>({});
const [newMetricPopoverVisible, setNewMetricPopoverVisible] = useState(false);
const prevColumns = usePrevious(columns);
const prevSavedMetrics = usePrevious(savedMetrics);
@@ -313,13 +316,16 @@ export const DndMetricSelect = (props: any) => {
);
const handleClickGhostButton = useCallback(() => {
setDroppedItem(null);
setDroppedItem({});
togglePopover(true);
}, [togglePopover]);
const adhocMetric = useMemo(() => {
if (droppedItem?.type === DndItemType.Column) {
const itemValue = droppedItem?.value as ColumnMeta;
if (
isDatasourcePanelDndItem(droppedItem) &&
droppedItem.type === DndItemType.Column
) {
const itemValue = droppedItem.value as ColumnMeta;
const config: Partial<AdhocMetric> = {
column: { column_name: itemValue?.column_name },
};

View File

@@ -31,7 +31,7 @@ test('Get ChartUri when allowDomainSharding:false', () => {
duplicateQueryParameters: false,
escapeQuerySpace: true,
fragment: null,
hostname: undefined,
hostname: 'localhost',
password: null,
path: '/path',
port: '',

View File

@@ -33,13 +33,13 @@ const createParams = () => ({
test('Get ExploreUrl with default params', () => {
const params = createParams();
expect(getExploreUrl(params)).toBe('http:///superset/explore/');
expect(getExploreUrl(params)).toBe('http://localhost/superset/explore/');
});
test('Get ExploreUrl with endpointType:full', () => {
const params = createParams();
expect(getExploreUrl({ ...params, endpointType: 'full' })).toBe(
'http:///superset/explore_json/',
'http://localhost/superset/explore_json/',
);
});
@@ -47,5 +47,5 @@ test('Get ExploreUrl with endpointType:full and method:GET', () => {
const params = createParams();
expect(
getExploreUrl({ ...params, endpointType: 'full', method: 'GET' }),
).toBe('http:///superset/explore_json/');
).toBe('http://localhost/superset/explore_json/');
});

View File

@@ -20,6 +20,7 @@
import {
AppSection,
DataMask,
DataRecordValue,
ensureIsArray,
ExtraFormData,
GenericDataType,
@@ -36,7 +37,11 @@ import { useImmerReducer } from 'use-immer';
import { FormItemProps } from 'antd/lib/form';
import { PluginFilterSelectProps, SelectValue } from './types';
import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common';
import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils';
import {
formatFilterValue,
getDataRecordFormatter,
getSelectExtraFormData,
} from '../../utils';
type DataMaskAction =
| { type: 'ownState'; ownState: JsonObject }
@@ -119,7 +124,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
filterState: {
...filterState,
label: values?.length
? `${(values || []).join(', ')}${suffix}`
? `${(values || []).map(formatFilterValue).join(', ')}${suffix}`
: undefined,
value:
appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem
@@ -249,12 +254,12 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
}
const options = useMemo(() => {
const options: { label: string; value: string | number }[] = [];
const options: { label: string; value: DataRecordValue }[] = [];
data.forEach(row => {
const [value] = groupby.map(col => row[col]);
options.push({
label: labelFormatter(value, datatype),
value: typeof value === 'number' ? value : String(value),
value,
});
});
return options;
@@ -286,6 +291,7 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
loading={isRefreshing}
maxTagCount={5}
invertSelection={inverseSelection}
// @ts-ignore
options={options}
/>
</StyledFormItem>

View File

@@ -28,7 +28,7 @@ import { FALSE_STRING, NULL_STRING, TRUE_STRING } from 'src/utils/common';
export const getSelectExtraFormData = (
col: string,
value?: null | (string | number)[],
value?: null | (string | number | boolean | null)[],
emptyFilter = false,
inverseSelection = false,
): ExtraFormData => {
@@ -46,6 +46,7 @@ export const getSelectExtraFormData = (
{
col,
op: inverseSelection ? ('NOT IN' as const) : ('IN' as const),
// @ts-ignore
val: value,
},
];
@@ -116,3 +117,18 @@ export function getDataRecordFormatter({
return String(value);
};
}
export function formatFilterValue(
value: string | number | boolean | null,
): string {
if (value === null) {
return NULL_STRING;
}
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return String(value);
}
return value ? TRUE_STRING : FALSE_STRING;
}

View File

@@ -57,6 +57,7 @@ import Chart from 'src/types/Chart';
import { Tooltip } from 'src/components/Tooltip';
import Icons from 'src/components/Icons';
import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils';
import setupPlugins from 'src/setup/setupPlugins';
import ChartCard from './ChartCard';
const PAGE_SIZE = 25;
@@ -73,6 +74,7 @@ const CONFIRM_OVERWRITE_MESSAGE = t(
'sure you want to overwrite?',
);
setupPlugins();
const registry = getChartMetadataRegistry();
const createFetchDatasets = (handleError: (err: Response) => void) => async (

View File

@@ -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}

View File

@@ -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"]

View File

@@ -27,7 +27,7 @@ from .helpers import get_example_data, get_table_connector_registry
def load_bart_lines(only_metadata: bool = False, force: bool = False) -> None:
tbl_name = "bart_lines"
tbl_name = "San Franciso BART Lines"
database = get_example_database()
table_exists = database.has_table_by_name(tbl_name)

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: FCC 2018 Survey
table_name: FCC Survey Results
main_dttm_col: null
description: null
default_endpoint: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: channel_members
table_name: Slack Channels and Members
main_dttm_col: null
description: null
default_endpoint: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: channels
table_name: Slack Channels
main_dttm_col: created
description: null
default_endpoint: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: cleaned_sales_data
table_name: Vehicle Sales
main_dttm_col: OrderDate
description: null
default_endpoint: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: covid_vaccines
table_name: COVID Vaccines
main_dttm_col: null
description: null
default_endpoint: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: exported_stats
table_name: Slack Exported Metrics
main_dttm_col: Date
description: null
default_endpoint: null

View File

@@ -14,15 +14,15 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: members_channels_2
table_name: Slack Members and Channels
main_dttm_col: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
schema: null
sql: SELECT c.name AS channel_name, u.name AS member_name FROM channel_members cm
JOIN channels c ON cm.channel_id = c.id JOIN users u ON cm.user_id = u.id
sql: SELECT c.name AS channel_name, u.name AS member_name FROM "Slack Channels and Members" cm
JOIN "Slack Channels" c ON cm.channel_id = c.id JOIN "Slack Users" u ON cm.user_id = u.id
params: null
template_params: null
filter_select_enabled: true

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: messages
table_name: Slack Messages
main_dttm_col: bot_profile__updated
description: null
default_endpoint: null

View File

@@ -14,14 +14,14 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: messages_channels
table_name: Slack Messages and Channels
main_dttm_col: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
schema: null
sql: SELECT m.ts, c.name, m.text FROM messages m JOIN channels c ON m.channel_id =
sql: SELECT m.ts, c.name, m.text FROM "Slack Messages" m JOIN "Slack Channels" c ON m.channel_id =
c.id
params: null
template_params: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: new_members_daily
table_name: Slack Daily Member Count
main_dttm_col: null
description: null
default_endpoint: null
@@ -22,7 +22,7 @@ offset: 0
cache_timeout: null
schema: null
sql: SELECT date, total_membership - lag(total_membership) OVER (ORDER BY date) AS
new_members FROM exported_stats
new_members FROM "Slack Exported Metrics"
params: null
template_params: null
filter_select_enabled: true

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: threads
table_name: Slack Threads
main_dttm_col: ts
description: null
default_endpoint: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: users
table_name: Slack Users
main_dttm_col: updated
description: null
default_endpoint: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: users_channels-uzooNNtSRO
table_name: Slack Channel Combinations and Users
main_dttm_col: null
description: null
default_endpoint: null
@@ -23,8 +23,8 @@ cache_timeout: null
schema: null
sql: >
SELECT uc1.name as channel_1, uc2.name as channel_2, count(*) AS cnt
FROM users_channels uc1
JOIN users_channels uc2 ON uc1.user_id = uc2.user_id
FROM "Slack Users and Channels" uc1
JOIN "Slack Users and Channels" uc2 ON uc1.user_id = uc2.user_id
GROUP BY uc1.name, uc2.name
HAVING uc1.name <> uc2.name
params: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: users_channels
table_name: Slack Users and Channels
main_dttm_col: null
description: null
default_endpoint: null

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: video_game_sales
table_name: Video Game Sales
main_dttm_col: null
description: null
default_endpoint: null

View File

@@ -176,7 +176,7 @@ def load_deck_dash() -> None:
print("Loading deck.gl dashboard")
slices = []
table = get_table_connector_registry()
tbl = db.session.query(table).filter_by(table_name="long_lat").first()
tbl = db.session.query(table).filter_by(table_name="Sample Geodata").first()
slice_data = {
"spatial": {"type": "latlong", "lonCol": "LON", "latCol": "LAT"},
"color_picker": COLOR_RED,
@@ -324,7 +324,9 @@ def load_deck_dash() -> None:
slices.append(slc)
polygon_tbl = (
db.session.query(table).filter_by(table_name="sf_population_polygons").first()
db.session.query(table)
.filter_by(table_name="San Francisco Population Polygons")
.first()
)
slice_data = {
"datasource": "11__table",
@@ -457,7 +459,7 @@ def load_deck_dash() -> None:
viz_type="deck_arc",
datasource_type="table",
datasource_id=db.session.query(table)
.filter_by(table_name="flights")
.filter_by(table_name="Flights")
.first()
.id,
params=get_slice_json(slice_data),
@@ -509,7 +511,7 @@ def load_deck_dash() -> None:
viz_type="deck_path",
datasource_type="table",
datasource_id=db.session.query(table)
.filter_by(table_name="bart_lines")
.filter_by(table_name="San Franciso BART Lines")
.first()
.id,
params=get_slice_json(slice_data),

View File

@@ -56,7 +56,7 @@ def load_energy(
method="multi",
)
print("Creating table [wb_health_population] reference")
print("Creating table [World Bank Health Data] reference")
table = get_table_connector_registry()
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:

View File

@@ -25,7 +25,7 @@ from .helpers import get_example_data, get_table_connector_registry
def load_flights(only_metadata: bool = False, force: bool = False) -> None:
"""Loading random time series data from a zip file in the repo"""
tbl_name = "flights"
tbl_name = "Flights"
database = utils.get_example_database()
table_exists = database.has_table_by_name(tbl_name)

View File

@@ -36,7 +36,7 @@ from .helpers import (
def load_long_lat_data(only_metadata: bool = False, force: bool = False) -> None:
"""Loading lat/long data from a csv file in the repo"""
tbl_name = "long_lat"
tbl_name = "Sample Geodata"
database = utils.get_example_database()
table_exists = database.has_table_by_name(tbl_name)

View File

@@ -28,7 +28,7 @@ from .helpers import get_example_data, get_table_connector_registry
def load_sf_population_polygons(
only_metadata: bool = False, force: bool = False
) -> None:
tbl_name = "sf_population_polygons"
tbl_name = "San Francisco Population Polygons"
database = utils.get_example_database()
table_exists = database.has_table_by_name(tbl_name)

View File

@@ -45,7 +45,7 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals
only_metadata: bool = False, force: bool = False, sample: bool = False,
) -> None:
"""Loads the world bank health dataset, slices and a dashboard"""
tbl_name = "wb_health_population"
tbl_name = "World Bank Health Data"
database = utils.get_example_database()
table_exists = database.has_table_by_name(tbl_name)
@@ -76,7 +76,7 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals
index=False,
)
print("Creating table [wb_health_population] reference")
print("Creating table [World Bank Health Data] reference")
table = get_table_connector_registry()
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:

View File

@@ -457,7 +457,7 @@ def cast_to_num(value: Optional[Union[float, int, str]]) -> Optional[Union[float
return None
def cast_to_boolean(value: Any) -> bool:
def cast_to_boolean(value: Any) -> Optional[bool]:
"""Casts a value to an int/float
>>> cast_to_boolean(1)
@@ -473,12 +473,13 @@ def cast_to_boolean(value: Any) -> bool:
>>> cast_to_boolean('False')
False
>>> cast_to_boolean(None)
False
:param value: value to be converted to boolean representation
:returns: value cast to `bool`. when value is 'true' or value that are not 0
converte into True
converted into True. Return `None` if value is `None`
"""
if value is None:
return None
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):

View File

@@ -1061,14 +1061,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
@@ -1081,7 +1075,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)
@@ -1123,15 +1116,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 = (
@@ -3039,12 +3026,12 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
.first()
)
databases: Dict[int, Any] = {
database.id: {
databases: Dict[int, Any] = {}
for database in DatabaseDAO.find_all():
databases[database.id] = {
k: v for k, v in database.to_json().items() if k in DATABASE_KEYS
}
for database in DatabaseDAO.find_all()
}
databases[database.id]["backend"] = database.backend
queries: Dict[str, Any] = {}
# These are unnecessary if sqllab backend persistence is disabled

View File

@@ -18,7 +18,7 @@ import json
from collections import Counter
from typing import Any
from flask import request
from flask import g, request
from flask_appbuilder import expose
from flask_appbuilder.api import rison
from flask_appbuilder.security.decorators import has_access_api
@@ -28,6 +28,7 @@ from sqlalchemy.exc import NoSuchTableError
from sqlalchemy.orm.exc import NoResultFound
from superset import app, db, event_logger
from superset.commands.utils import populate_owners
from superset.connectors.connector_registry import ConnectorRegistry
from superset.connectors.sqla.utils import get_physical_table_metadata
from superset.datasets.commands.exceptions import (
@@ -35,6 +36,7 @@ from superset.datasets.commands.exceptions import (
DatasetNotFoundError,
)
from superset.exceptions import SupersetException, SupersetSecurityException
from superset.extensions import security_manager
from superset.models.core import Database
from superset.typing import FlaskResponse
from superset.views.base import (
@@ -79,16 +81,28 @@ class Datasource(BaseSupersetView):
if "owners" in datasource_dict and orm_datasource.owner_class is not None:
# Check ownership
if app.config["OLD_API_CHECK_DATASET_OWNERSHIP"]:
# mimic the behavior of the new dataset command that
# checks ownership and ensures that non-admins aren't locked out
# of the object
try:
check_ownership(orm_datasource)
except SupersetSecurityException as ex:
raise DatasetForbiddenError() from ex
datasource_dict["owners"] = (
db.session.query(orm_datasource.owner_class)
.filter(orm_datasource.owner_class.id.in_(datasource_dict["owners"]))
.all()
)
user = security_manager.get_user_by_id(g.user.id)
datasource_dict["owners"] = populate_owners(
user, datasource_dict["owners"], default_to_user=False
)
else:
# legacy behavior
datasource_dict["owners"] = (
db.session.query(orm_datasource.owner_class)
.filter(
orm_datasource.owner_class.id.in_(
datasource_dict["owners"] or []
)
)
.all()
)
duplicates = [
name

View File

@@ -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,10 +244,9 @@ 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
assert {
k: v for k, v in response["result"].items() if k in expected_result
} == expected_result
assert len(response["result"]["columns"]) == 3
assert len(response["result"]["metrics"]) == 2