Compare commits

...

10 Commits

Author SHA1 Message Date
Elizabeth Thompson
1345bbe26b fix: return query if it already exists (#15207)
* check if query exists before saving a new one

* fix test

(cherry picked from commit 58cc78d2c1)
2021-06-23 00:18:36 -07:00
Hugh A. Miles II
6fc6a4f9db fix: Fix dremio dialect not having a driver field (#15198)
* Update __init__.py

* Update superset/db_engine_specs/__init__.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
(cherry picked from commit 965dacdb33)
2021-06-16 17:49:15 -07:00
Beto Dealmeida
31fd556091 fix: ignore errors in GetLog (#15181)
(cherry picked from commit ffdbcbd16e)
2021-06-16 17:49:15 -07:00
Elizabeth Thompson
018c0a9157 add another wait for chart element (#15108)
(cherry picked from commit 7dc0cee5be)
2021-06-15 10:50:59 -07:00
Kamil Gabryjelski
34f6fd0546 fix(native-filters): Don't send unnecessary PUT request on dashboard render (#15146)
* fix(native-filters): Don't send unnecessary PUT request on dashboard render

* Run native filters scopes only if feature flag is enabled

* Change action name

* Run native filters scopes only if at least 1 filter added

* Fix lint

(cherry picked from commit 3866044938)
2021-06-15 10:50:59 -07:00
Ville Brofeldt
d986b46cf6 fix(native-filters): empty label indicator (#15084)
(cherry picked from commit c0eff8faf6)
2021-06-15 10:50:59 -07:00
Ville Brofeldt
b1d1f56a60 fix(native-filters): remove hard-coded default time range (#15015)
* fix(native-filters): use default for time range from explore

* fix tests

(cherry picked from commit 8aaa6036d7)
2021-06-15 10:50:59 -07:00
Beto Dealmeida
91f443e6b4 fix: confirm overwrite and password on import (#15056)
* fix: confirm overwrite and password on import

* Add tests

(cherry picked from commit 4d24d4dc9a)
2021-06-15 10:50:58 -07:00
Elizabeth Thompson
690a4bfb07 move metric parsing to state instantiation (#15069)
(cherry picked from commit 0c470feaef)
2021-06-15 10:50:58 -07:00
Beto Dealmeida
5832cf0080 fix: edit BQ w/o encrypted_extra (#15048)
* fix: edit BQ w/o encrypted_extra

* Fix lint

(cherry picked from commit a59bbbc544)
2021-06-15 10:50:58 -07:00
22 changed files with 361 additions and 123 deletions

View File

@@ -233,4 +233,21 @@ describe('DatasourceEditor RTL', () => {
);
expect(warningMarkdown.value).toEqual('someone');
});
it('properly updates the metric information', async () => {
render(<DatasourceEditor {...props} />, {
useRedux: true,
});
const metricButton = screen.getByTestId('collection-tab-Metrics');
userEvent.click(metricButton);
const expandToggle = await screen.findAllByLabelText(/toggle expand/i);
userEvent.click(expandToggle[1]);
const certifiedBy = await screen.findByPlaceholderText(/certified by/i);
userEvent.type(certifiedBy, 'I am typing a new name');
const certificationDetails = await screen.findByPlaceholderText(
/certification details/i,
);
expect(certifiedBy.value).toEqual('I am typing a new name');
userEvent.type(certificationDetails, 'I am typing something new');
expect(certificationDetails.value).toEqual('I am typing something new');
});
});

View File

@@ -49,6 +49,11 @@ export interface SetFilterConfigFail {
type: typeof SET_FILTER_CONFIG_FAIL;
filterConfig: FilterConfiguration;
}
export const SET_IN_SCOPE_STATUS_OF_FILTERS = 'SET_IN_SCOPE_STATUS_OF_FILTERS';
export interface SetInScopeStatusOfFilters {
type: typeof SET_IN_SCOPE_STATUS_OF_FILTERS;
filterConfig: FilterConfiguration;
}
export const SET_FILTER_SETS_CONFIG_BEGIN = 'SET_FILTER_SETS_CONFIG_BEGIN';
export interface SetFilterSetsConfigBegin {
type: typeof SET_FILTER_SETS_CONFIG_BEGIN;
@@ -124,6 +129,25 @@ export const setFilterConfiguration = (
}
};
export const setInScopeStatusOfFilters = (
filterScopes: {
filterId: string;
chartsInScope: number[];
tabsInScope: string[];
}[],
) => async (dispatch: Dispatch, getState: () => any) => {
const filters = getState().nativeFilters?.filters;
const filtersWithScopes = filterScopes.map(scope => ({
...filters[scope.filterId],
chartsInScope: scope.chartsInScope,
tabsInScope: scope.tabsInScope,
}));
dispatch({
type: SET_IN_SCOPE_STATUS_OF_FILTERS,
filterConfig: filtersWithScopes,
});
};
type BootstrapData = {
nativeFilters: {
filters: Filters;
@@ -227,6 +251,7 @@ export type AnyFilterAction =
| SetFilterSetsConfigBegin
| SetFilterSetsConfigComplete
| SetFilterSetsConfigFail
| SetInScopeStatusOfFilters
| SaveFilterSets
| SetBootstrapData
| SetFocusedNativeFilter

View File

@@ -18,10 +18,11 @@
*/
// ParentSize uses resize observer so the dashboard will update size
// when its container size changes, due to e.g., builder side panel opening
import { ParentSize } from '@vx/responsive';
import Tabs from 'src/components/Tabs';
import React, { FC, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { FeatureFlag, isFeatureEnabled } from '@superset-ui/core';
import { ParentSize } from '@vx/responsive';
import Tabs from 'src/components/Tabs';
import DashboardGrid from 'src/dashboard/containers/DashboardGrid';
import getLeafComponentIdFromPath from 'src/dashboard/util/getLeafComponentIdFromPath';
import { DashboardLayout, LayoutItem, RootState } from 'src/dashboard/types';
@@ -33,7 +34,7 @@ import { getRootLevelTabIndex } from './utils';
import { Filters } from '../../reducers/types';
import { getChartIdsInFilterScope } from '../../util/activeDashboardFilters';
import { findTabsWithChartsInScope } from '../nativeFilters/utils';
import { setFilterConfiguration } from '../../actions/nativeFilters';
import { setInScopeStatusOfFilters } from '../../actions/nativeFilters';
type DashboardContainerProps = {
topLevelTabs?: LayoutItem;
@@ -43,9 +44,9 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const dashboardLayout = useSelector<RootState, DashboardLayout>(
state => state.dashboardLayout.present,
);
const nativeFilters = useSelector<RootState, Filters>(
state => state.nativeFilters.filters,
);
const nativeFilters =
useSelector<RootState, Filters>(state => state.nativeFilters?.filters) ??
{};
const directPathToChild = useSelector<RootState, string[]>(
state => state.dashboardState.directPathToChild,
);
@@ -63,9 +64,15 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const nativeFiltersValues = Object.values(nativeFilters);
const scopes = nativeFiltersValues.map(filter => filter.scope);
useEffect(() => {
nativeFiltersValues.forEach(filter => {
if (
!isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) ||
nativeFiltersValues.length === 0
) {
return;
}
const filterScopes = nativeFiltersValues.map(filter => {
const filterScope = filter.scope;
const chartsInScope = getChartIdsInFilterScope({
const chartsInScope: number[] = getChartIdsInFilterScope({
filterScope: {
scope: filterScope.rootPath,
// @ts-ignore
@@ -76,12 +83,13 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
dashboardLayout,
chartsInScope,
);
Object.assign(filter, {
chartsInScope,
return {
filterId: filter.id,
tabsInScope: Array.from(tabsInScope),
});
chartsInScope,
};
});
dispatch(setFilterConfiguration(nativeFiltersValues));
dispatch(setInScopeStatusOfFilters(filterScopes));
}, [JSON.stringify(scopes), JSON.stringify(dashboardLayout)]);
const childIds: string[] = topLevelTabs

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { TIME_FILTER_MAP } from 'src/explore/constants';
import { NO_TIME_RANGE, TIME_FILTER_MAP } from 'src/explore/constants';
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
import {
ChartConfiguration,
@@ -63,7 +63,7 @@ const selectIndicatorValue = (
if (
values == null ||
(filter.isDateFilter && values === 'No filter') ||
(filter.isDateFilter && values === NO_TIME_RANGE) ||
arrValues.length === 0
) {
return [];

View File

@@ -21,20 +21,22 @@ import React from 'react';
import PropTypes from 'prop-types';
import { styled } from '@superset-ui/core';
import { exploreChart, exportChart } from '../../../explore/exploreUtils';
import SliceHeader from '../SliceHeader';
import ChartContainer from '../../../chart/ChartContainer';
import MissingChart from '../MissingChart';
import { slicePropShape, chartPropShape } from '../../util/propShapes';
import { exploreChart, exportChart } from 'src/explore/exploreUtils';
import ChartContainer from 'src/chart/ChartContainer';
import {
LOG_ACTIONS_CHANGE_DASHBOARD_FILTER,
LOG_ACTIONS_EXPLORE_DASHBOARD_CHART,
LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART,
LOG_ACTIONS_FORCE_REFRESH_CHART,
} from '../../../logger/LogUtils';
} from 'src/logger/LogUtils';
import { areObjectsEqual } from 'src/reduxUtils';
import SliceHeader from '../SliceHeader';
import MissingChart from '../MissingChart';
import { slicePropShape, chartPropShape } from '../../util/propShapes';
import { isFilterBox } from '../../util/activeDashboardFilters';
import getFilterValuesByFilterId from '../../util/getFilterValuesByFilterId';
import { areObjectsEqual } from '../../../reduxUtils';
const propTypes = {
id: PropTypes.number.isRequired,

View File

@@ -91,7 +91,7 @@ const addFilterSetFlow = async () => {
expect(screen.getByText('Filters (1)')).toBeInTheDocument();
expect(screen.getByText(FILTER_NAME)).toBeInTheDocument();
expect(screen.getAllByText('Last week').length).toBe(2);
expect(screen.getAllByText('No filter').length).toBe(1);
// apply filters
expect(screen.getByTestId(getTestId('new-filter-set-button'))).toBeEnabled();
@@ -109,7 +109,7 @@ const addFilterSetFlow = async () => {
};
const changeFilterValue = async () => {
userEvent.click(screen.getAllByText('Last week')[0]);
userEvent.click(screen.getAllByText('No filter')[0]);
userEvent.click(screen.getByDisplayValue('Last day'));
expect(await screen.findByText(/2021-04-13/)).toBeInTheDocument();
userEvent.click(screen.getByTestId(getDateControlTestId('apply-button')));
@@ -305,10 +305,11 @@ describe('FilterBar', () => {
await addFilterFlow();
expect(screen.getByTestId(getTestId('apply-button'))).toBeEnabled();
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
});
it('add and apply filter set', async () => {
// disable due to filter sets not detecting changes in metadata properly
it.skip('add and apply filter set', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_NATIVE_FILTERS]: true,
@@ -344,12 +345,13 @@ describe('FilterBar', () => {
).not.toHaveAttribute('data-selected', 'true');
userEvent.click(screen.getByTestId(getTestId('filter-set-wrapper')));
userEvent.click(screen.getAllByText('Filters (1)')[1]);
expect(await screen.findByText('Last week')).toBeInTheDocument();
expect(await screen.findByText('No filter')).toBeInTheDocument();
userEvent.click(screen.getByTestId(getTestId('apply-button')));
expect(screen.getByTestId(getTestId('apply-button'))).toBeDisabled();
});
it('add and edit filter set', async () => {
// disable due to filter sets not detecting changes in metadata properly
it.skip('add and edit filter set', async () => {
// @ts-ignore
global.featureFlags = {
[FeatureFlag.DASHBOARD_NATIVE_FILTERS_SET]: true,

View File

@@ -20,6 +20,7 @@ import {
AnyFilterAction,
SAVE_FILTER_SETS,
SET_FILTER_CONFIG_COMPLETE,
SET_IN_SCOPE_STATUS_OF_FILTERS,
SET_FILTER_SETS_CONFIG_COMPLETE,
SET_FOCUSED_NATIVE_FILTER,
UNSET_FOCUSED_NATIVE_FILTER,
@@ -92,6 +93,7 @@ export default function nativeFilterReducer(
};
case SET_FILTER_CONFIG_COMPLETE:
case SET_IN_SCOPE_STATUS_OF_FILTERS:
return getInitialState({ filterConfig: action.filterConfig, state });
case SET_FILTER_SETS_CONFIG_COMPLETE:

View File

@@ -307,7 +307,21 @@ class DatasourceEditor extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
datasource: props.datasource,
datasource: {
...props.datasource,
metrics: props.datasource.metrics?.map(metric => {
const {
certification: { details, certified_by: certifiedBy } = {},
warning_markdown: warningMarkdown,
} = JSON.parse(metric.extra || '{}') || {};
return {
...metric,
certification_details: details || '',
warning_markdown: warningMarkdown || '',
certified_by: certifiedBy,
};
}),
},
errors: [],
isDruid:
props.datasource.type === 'druid' ||
@@ -936,18 +950,7 @@ class DatasourceEditor extends React.PureComponent {
</Fieldset>
</FormContainer>
}
collection={this.state.datasource.metrics?.map(metric => {
const {
certification: { details, certified_by: certifiedBy } = {},
warning_markdown: warningMarkdown,
} = JSON.parse(metric.extra || '{}') || {};
return {
...metric,
certification_details: details || '',
warning_markdown: warningMarkdown || '',
certified_by: certifiedBy,
};
})}
collection={this.state.datasource.metrics}
allowAddItem
onChange={this.onDatasourcePropChange.bind(this, 'metrics')}
itemGenerator={() => ({

View File

@@ -107,3 +107,4 @@ export const TIME_FILTER_MAP = {
// TODO: make this configurable per Superset installation
export const DEFAULT_TIME_RANGE = 'No filter';
export const NO_TIME_RANGE = 'No filter';

View File

@@ -150,7 +150,7 @@ describe('SelectFilterPlugin', () => {
],
},
filterState: {
label: '',
label: undefined,
value: null,
},
});
@@ -165,7 +165,7 @@ describe('SelectFilterPlugin', () => {
},
extraFormData: {},
filterState: {
label: '',
label: undefined,
value: null,
},
});

View File

@@ -138,7 +138,9 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) {
inverseSelection,
),
filterState: {
label: `${(values || []).join(', ')}${suffix}`,
label: values?.length
? `${(values || []).join(', ')}${suffix}`
: undefined,
value:
appSection === AppSection.FILTER_CONFIG_MODAL && defaultToFirstItem
? undefined

View File

@@ -17,12 +17,11 @@
* under the License.
*/
import { styled } from '@superset-ui/core';
import React, { useState, useEffect } from 'react';
import React, { useEffect } from 'react';
import DateFilterControl from 'src/explore/components/controls/DateFilterControl';
import { PluginFilterTimeProps } from './types';
import { Styles } from '../common';
const DEFAULT_VALUE = 'Last week';
import { NO_TIME_RANGE } from '../../../explore/constants';
const TimeFilterStyles = styled(Styles)`
overflow-x: scroll;
@@ -34,36 +33,31 @@ const ControlContainer = styled.div`
export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
const {
formData,
setDataMask,
setFocusedFilter,
unsetFocusedFilter,
width,
filterState,
} = props;
const { defaultValue } = formData;
const [value, setValue] = useState<string>(defaultValue ?? DEFAULT_VALUE);
const handleTimeRangeChange = (timeRange: string): void => {
setValue(timeRange);
const handleTimeRangeChange = (timeRange?: string): void => {
const isSet = timeRange && timeRange !== NO_TIME_RANGE;
setDataMask({
extraFormData: {
time_range: timeRange,
extraFormData: isSet
? {
time_range: timeRange,
}
: {},
filterState: {
value: isSet ? timeRange : undefined,
},
filterState: { value: timeRange },
});
};
useEffect(() => {
handleTimeRangeChange(filterState.value ?? DEFAULT_VALUE);
handleTimeRangeChange(filterState.value);
}, [filterState.value]);
useEffect(() => {
handleTimeRangeChange(defaultValue ?? DEFAULT_VALUE);
}, [defaultValue]);
return (
// @ts-ignore
<TimeFilterStyles width={width}>
@@ -72,7 +66,7 @@ export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
onMouseLeave={unsetFocusedFilter}
>
<DateFilterControl
value={value}
value={filterState.value}
name="time_range"
onChange={handleTimeRangeChange}
/>

View File

@@ -20,7 +20,12 @@ import rison from 'rison';
import { useState, useEffect, useCallback } from 'react';
import { makeApi, SupersetClient, t, JsonObject } from '@superset-ui/core';
import { createErrorHandler } from 'src/views/CRUD/utils';
import {
createErrorHandler,
getAlreadyExists,
getPasswordsNeeded,
hasTerminalValidation,
} from 'src/views/CRUD/utils';
import { FetchDataConfig } from 'src/components/ListView';
import { FilterValue } from 'src/components/ListView/types';
import Chart, { Slice } from 'src/types/Chart';
@@ -384,40 +389,6 @@ export function useImportResource(
setState(currentState => ({ ...currentState, ...update }));
}
/* eslint-disable no-underscore-dangle */
const isNeedsPassword = (payload: any) =>
typeof payload === 'object' &&
Array.isArray(payload._schema) &&
payload._schema.length === 1 &&
payload._schema[0] === 'Must provide a password for the database';
const isAlreadyExists = (payload: any) =>
typeof payload === 'string' &&
payload.includes('already exists and `overwrite=true` was not passed');
const getPasswordsNeeded = (
errMsg: Record<string, Record<string, string[] | string>>,
) =>
Object.entries(errMsg)
.filter(([, validationErrors]) => isNeedsPassword(validationErrors))
.map(([fileName]) => fileName);
const getAlreadyExists = (
errMsg: Record<string, Record<string, string[] | string>>,
) =>
Object.entries(errMsg)
.filter(([, validationErrors]) => isAlreadyExists(validationErrors))
.map(([fileName]) => fileName);
const hasTerminalValidation = (
errMsg: Record<string, Record<string, string[] | string>>,
) =>
Object.values(errMsg).some(
validationErrors =>
!isNeedsPassword(validationErrors) &&
!isAlreadyExists(validationErrors),
);
const importResource = useCallback(
(
bundle: File,
@@ -452,29 +423,28 @@ export function useImportResource(
.then(() => true)
.catch(response =>
getClientErrorObject(response).then(error => {
const errMsg = error.message || error.error;
if (typeof errMsg === 'string') {
if (!error.errors) {
handleErrorMsg(
t(
'An error occurred while importing %s: %s',
resourceLabel,
parsedErrorMessage(errMsg),
error.message || error.error,
),
);
return false;
}
if (hasTerminalValidation(errMsg)) {
if (hasTerminalValidation(error.errors)) {
handleErrorMsg(
t(
'An error occurred while importing %s: %s',
resourceLabel,
parsedErrorMessage(errMsg),
error.errors.map(payload => payload.message).join('\n'),
),
);
} else {
updateState({
passwordsNeeded: getPasswordsNeeded(errMsg),
alreadyExists: getAlreadyExists(errMsg),
passwordsNeeded: getPasswordsNeeded(error.errors),
alreadyExists: getAlreadyExists(error.errors),
});
}
return false;

View File

@@ -0,0 +1,145 @@
/**
* 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 {
isNeedsPassword,
isAlreadyExists,
getPasswordsNeeded,
getAlreadyExists,
hasTerminalValidation,
} from 'src/views/CRUD/utils';
const terminalErrors = {
errors: [
{
message: 'Error importing database',
error_type: 'GENERIC_COMMAND_ERROR',
level: 'warning',
extra: {
'metadata.yaml': { type: ['Must be equal to Database.'] },
issue_codes: [
{
code: 1010,
message:
'Issue 1010 - Superset encountered an error while running a command.',
},
],
},
},
],
};
const overwriteNeededErrors = {
errors: [
{
message: 'Error importing database',
error_type: 'GENERIC_COMMAND_ERROR',
level: 'warning',
extra: {
'databases/imported_database.yaml':
'Database already exists and `overwrite=true` was not passed',
issue_codes: [
{
code: 1010,
message:
'Issue 1010 - Superset encountered an error while running a command.',
},
],
},
},
],
};
const passwordNeededErrors = {
errors: [
{
message: 'Error importing database',
error_type: 'GENERIC_COMMAND_ERROR',
level: 'warning',
extra: {
'databases/imported_database.yaml': {
_schema: ['Must provide a password for the database'],
},
issue_codes: [
{
code: 1010,
message:
'Issue 1010 - Superset encountered an error while running a command.',
},
],
},
},
],
};
test('identifies error payloads indicating that password is needed', () => {
let needsPassword;
needsPassword = isNeedsPassword({
_schema: ['Must provide a password for the database'],
});
expect(needsPassword).toBe(true);
needsPassword = isNeedsPassword(
'Database already exists and `overwrite=true` was not passed',
);
expect(needsPassword).toBe(false);
needsPassword = isNeedsPassword({ type: ['Must be equal to Database.'] });
expect(needsPassword).toBe(false);
});
test('identifies error payloads indicating that overwrite confirmation is needed', () => {
let alreadyExists;
alreadyExists = isAlreadyExists(
'Database already exists and `overwrite=true` was not passed',
);
expect(alreadyExists).toBe(true);
alreadyExists = isAlreadyExists({
_schema: ['Must provide a password for the database'],
});
expect(alreadyExists).toBe(false);
alreadyExists = isAlreadyExists({ type: ['Must be equal to Database.'] });
expect(alreadyExists).toBe(false);
});
test('extracts DB configuration files that need passwords', () => {
const passwordsNeeded = getPasswordsNeeded(passwordNeededErrors.errors);
expect(passwordsNeeded).toEqual(['databases/imported_database.yaml']);
});
test('extracts files that need overwrite confirmation', () => {
const alreadyExists = getAlreadyExists(overwriteNeededErrors.errors);
expect(alreadyExists).toEqual(['databases/imported_database.yaml']);
});
test('detects if the error message is terminal or if it requires uses intervention', () => {
let isTerminal;
isTerminal = hasTerminalValidation(terminalErrors.errors);
expect(isTerminal).toBe(true);
isTerminal = hasTerminalValidation(overwriteNeededErrors.errors);
expect(isTerminal).toBe(false);
isTerminal = hasTerminalValidation(passwordNeededErrors.errors);
expect(isTerminal).toBe(false);
});

View File

@@ -322,3 +322,40 @@ export const CardStyles = styled.div`
text-decoration: none;
}
`;
export /* eslint-disable no-underscore-dangle */
const isNeedsPassword = (payload: any) =>
typeof payload === 'object' &&
Array.isArray(payload._schema) &&
payload._schema.length === 1 &&
payload._schema[0] === 'Must provide a password for the database';
export const isAlreadyExists = (payload: any) =>
typeof payload === 'string' &&
payload.includes('already exists and `overwrite=true` was not passed');
export const getPasswordsNeeded = (errors: Record<string, any>[]) =>
errors
.map(error =>
Object.entries(error.extra)
.filter(([, payload]) => isNeedsPassword(payload))
.map(([fileName]) => fileName),
)
.flat();
export const getAlreadyExists = (errors: Record<string, any>[]) =>
errors
.map(error =>
Object.entries(error.extra)
.filter(([, payload]) => isAlreadyExists(payload))
.map(([fileName]) => fileName),
)
.flat();
export const hasTerminalValidation = (errors: Record<string, any>[]) =>
errors.some(
error =>
!Object.values(error.extra).some(
payload => isNeedsPassword(payload) || isAlreadyExists(payload),
),
);

View File

@@ -121,7 +121,7 @@ def get_available_engine_specs() -> Dict[Type[BaseEngineSpec], Set[str]]:
except Exception: # pylint: disable=broad-except
logger.warning("Unable to load SQLAlchemy dialect: %s", dialect)
else:
drivers[dialect.name].add(dialect.driver)
drivers[dialect.name].add(getattr(dialect, "driver", dialect.name))
engine_specs = get_engine_specs()
return {

View File

@@ -331,7 +331,12 @@ class HiveEngineSpec(PrestoEngineSpec):
cursor.cancel()
break
log = cursor.fetch_logs() or ""
try:
log = cursor.fetch_logs() or ""
except Exception: # pylint: disable=broad-except
logger.warning("Call to GetLog() failed")
log = ""
if log:
log_lines = log.splitlines()
progress = cls.progress(log_lines)

View File

@@ -239,15 +239,12 @@ class Database(
@property
def parameters(self) -> Dict[str, Any]:
# Build parameters if db_engine_spec is a subclass of BasicParametersMixin
parameters = {"engine": self.backend}
if hasattr(self.db_engine_spec, "parameters_schema") and hasattr(
self.db_engine_spec, "get_parameters_from_uri"
):
uri = make_url(self.sqlalchemy_uri_decrypted)
encrypted_extra = self.get_encrypted_extra()
return {**parameters, **self.db_engine_spec.get_parameters_from_uri(uri, encrypted_extra=encrypted_extra)} # type: ignore
uri = make_url(self.sqlalchemy_uri_decrypted)
encrypted_extra = self.get_encrypted_extra()
try:
parameters = self.db_engine_spec.get_parameters_from_uri(uri, encrypted_extra=encrypted_extra) # type: ignore
except Exception: # pylint: disable=broad-except
parameters = {}
return parameters

View File

@@ -114,6 +114,12 @@ class WebDriverProxy:
WebDriverWait(driver, self._screenshot_load_wait).until_not(
EC.presence_of_all_elements_located((By.CLASS_NAME, "loading"))
)
logger.debug("Wait for chart to have content")
WebDriverWait(driver, self._screenshot_locate_wait).until(
EC.visibility_of_all_elements_located(
(By.CLASS_NAME, "slice_container")
)
)
logger.info("Taking a PNG screenshot or url %s", url)
img = element.screenshot_as_png
except TimeoutException:

View File

@@ -2467,14 +2467,34 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
CtasMethod, query_params.get("ctas_method", CtasMethod.TABLE)
)
tmp_table_name: str = cast(str, query_params.get("tmp_table_name"))
client_id: str = cast(
str, query_params.get("client_id") or utils.shortid()[:10]
)
client_id: str = cast(str, query_params.get("client_id"))
client_id_or_short_id: str = cast(str, client_id or utils.shortid()[:10])
sql_editor_id: str = cast(str, query_params.get("sql_editor_id"))
tab_name: str = cast(str, query_params.get("tab"))
status: str = QueryStatus.PENDING if async_flag else QueryStatus.RUNNING
user_id: int = g.user.get_id() if g.user else None
session = db.session()
# check to see if this query is already running
query = (
session.query(Query)
.filter_by(
client_id=client_id, user_id=user_id, sql_editor_id=sql_editor_id
)
.one_or_none()
)
if query is not None and query.status in [
QueryStatus.RUNNING,
QueryStatus.PENDING,
QueryStatus.TIMED_OUT,
]:
# return the existing query
payload = json.dumps(
{"query": query.to_dict()}, default=utils.json_int_dttm_ser
)
return json_success(payload)
mydb = session.query(Database).get(database_id)
if not mydb:
return json_error_response("Database with id %i is missing.", database_id)
@@ -2502,8 +2522,8 @@ class Superset(BaseSupersetView): # pylint: disable=too-many-public-methods
sql_editor_id=sql_editor_id,
tmp_table_name=tmp_table_name,
tmp_schema_name=tmp_schema_name,
user_id=g.user.get_id() if g.user else None,
client_id=client_id,
user_id=user_id,
client_id=client_id_or_short_id,
)
try:
session.add(query)

View File

@@ -228,10 +228,12 @@ class TabStateView(BaseSupersetView):
@has_access_api
@expose("<int:tab_state_id>/query/<client_id>", methods=["DELETE"])
def delete_query( # pylint: disable=no-self-use
self, tab_state_id: str, client_id: str
self, tab_state_id: int, client_id: str
) -> FlaskResponse:
db.session.query(Query).filter_by(
client_id=client_id, user_id=g.user.get_id(), sql_editor_id=tab_state_id
client_id=client_id,
user_id=g.user.get_id(),
sql_editor_id=str(tab_state_id),
).delete(synchronize_session=False)
db.session.commit()
return json_success(json.dumps("OK"))

View File

@@ -1412,7 +1412,7 @@ class TestCore(SupersetTestCase):
"client_id_1",
user_name=username,
raise_on_error=True,
sql_editor_id=tab_state_id,
sql_editor_id=str(tab_state_id),
)
# run an orphan query (no tab)
self.run_sql(