mirror of
https://github.com/apache/superset.git
synced 2026-04-30 13:34:20 +00:00
Compare commits
10 Commits
fix/postgr
...
v2021.23.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1345bbe26b | ||
|
|
6fc6a4f9db | ||
|
|
31fd556091 | ||
|
|
018c0a9157 | ||
|
|
34f6fd0546 | ||
|
|
d986b46cf6 | ||
|
|
b1d1f56a60 | ||
|
|
91f443e6b4 | ||
|
|
690a4bfb07 | ||
|
|
5832cf0080 |
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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={() => ({
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
145
superset-frontend/src/views/CRUD/utils.test.tsx
Normal file
145
superset-frontend/src/views/CRUD/utils.test.tsx
Normal 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);
|
||||
});
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user