diff --git a/.pylintrc b/.pylintrc index e69d5c4844..0835b7afc0 100644 --- a/.pylintrc +++ b/.pylintrc @@ -81,7 +81,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=standarderror-builtin,long-builtin,dict-view-method,intern-builtin,suppressed-message,no-absolute-import,unpacking-in-except,apply-builtin,delslice-method,indexing-exception,old-raise-syntax,print-statement,cmp-builtin,reduce-builtin,useless-suppression,coerce-method,input-builtin,cmp-method,raw_input-builtin,nonzero-method,backtick,basestring-builtin,setslice-method,reload-builtin,oct-method,map-builtin-not-iterating,execfile-builtin,old-octal-literal,zip-builtin-not-iterating,buffer-builtin,getslice-method,metaclass-assignment,xrange-builtin,long-suffix,round-builtin,range-builtin-not-iterating,next-method-called,parameter-unpacking,unicode-builtin,unichr-builtin,import-star-module-level,raising-string,filter-builtin-not-iterating,using-cmp-argument,coerce-builtin,file-builtin,old-division,hex-method,missing-docstring,too-many-lines,ungrouped-imports,import-outside-toplevel,raise-missing-from,super-with-arguments,bad-option-value +disable=long-builtin,dict-view-method,intern-builtin,suppressed-message,no-absolute-import,unpacking-in-except,apply-builtin,delslice-method,indexing-exception,old-raise-syntax,print-statement,cmp-builtin,reduce-builtin,useless-suppression,coerce-method,input-builtin,cmp-method,raw_input-builtin,nonzero-method,backtick,basestring-builtin,setslice-method,reload-builtin,oct-method,map-builtin-not-iterating,execfile-builtin,old-octal-literal,zip-builtin-not-iterating,buffer-builtin,getslice-method,metaclass-assignment,xrange-builtin,long-suffix,round-builtin,range-builtin-not-iterating,next-method-called,parameter-unpacking,unicode-builtin,unichr-builtin,import-star-module-level,raising-string,filter-builtin-not-iterating,using-cmp-argument,coerce-builtin,file-builtin,old-division,hex-method,missing-docstring,too-many-lines,ungrouped-imports,import-outside-toplevel,raise-missing-from,super-with-arguments,bad-option-value [REPORTS] diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js index 0ee38568f3..c2f918439f 100644 --- a/superset-frontend/.eslintrc.js +++ b/superset-frontend/.eslintrc.js @@ -122,6 +122,7 @@ module.exports = { 'padded-blocks': 0, 'prefer-arrow-callback': 0, 'prefer-destructuring': ['error', { object: true, array: false }], + 'react/default-props-match-prop-types': 0, // disabled temporarily 'react/destructuring-assignment': 0, // re-enable up for discussion 'react/forbid-prop-types': 0, 'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }], @@ -232,6 +233,7 @@ module.exports = { 'prefer-arrow-callback': 0, 'prefer-object-spread': 1, 'prefer-destructuring': ['error', { object: true, array: false }], + 'react/default-props-match-prop-types': 0, // disabled temporarily 'react/destructuring-assignment': 0, // re-enable up for discussion 'react/forbid-prop-types': 0, 'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }], diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts index f272c3804a..60debb6fb4 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard_list/filter.test.ts @@ -18,7 +18,7 @@ */ import { DASHBOARD_LIST } from './dashboard_list.helper'; -describe('dashboard filters', () => { +describe('dashboard filters card view', () => { beforeEach(() => { cy.login(); cy.server(); @@ -36,14 +36,63 @@ describe('dashboard filters', () => { cy.get('.ant-card').should('not.exist'); }); + it('should filter by created by correctly', () => { + // filter by created by + cy.get('.Select__control').eq(1).click(); + cy.get('.Select__menu').contains('alpha user').click(); + cy.get('.ant-card').should('not.exist'); + cy.get('.Select__control').eq(1).click(); + cy.get('.Select__menu').contains('gamma user').click(); + cy.get('.ant-card').should('not.exist'); + }); + it('should filter by published correctly', () => { // filter by published - cy.get('.Select__control').eq(1).click(); + cy.get('.Select__control').eq(2).click(); cy.get('.Select__menu').contains('Published').click(); cy.get('.ant-card').should('have.length', 2); cy.get('.ant-card').first().contains('USA Births Names').should('exist'); - cy.get('.Select__control').eq(1).click(); - cy.get('.Select__control').eq(1).type('unpub{enter}'); + cy.get('.Select__control').eq(2).click(); + cy.get('.Select__control').eq(2).type('unpub{enter}'); cy.get('.ant-card').should('have.length', 2); }); }); + +describe('dashboard filters list view', () => { + beforeEach(() => { + cy.login(); + cy.server(); + cy.visit(DASHBOARD_LIST); + }); + + it('should filter by owners correctly', () => { + // filter by owners + cy.get('.Select__control').first().click(); + cy.get('.Select__menu').contains('alpha user').click(); + cy.get('.table-row').should('not.exist'); + cy.get('.Select__control').first().click(); + cy.get('.Select__menu').contains('gamma user').click(); + cy.get('.table-row').should('not.exist'); + }); + + it('should filter by created by correctly', () => { + // filter by created by + cy.get('.Select__control').eq(1).click(); + cy.get('.Select__menu').contains('alpha user').click(); + cy.get('.table-row').should('not.exist'); + cy.get('.Select__control').eq(1).click(); + cy.get('.Select__menu').contains('gamma user').click(); + cy.get('.table-row').should('not.exist'); + }); + + it('should filter by published correctly', () => { + // filter by published + cy.get('.Select__control').eq(2).click(); + cy.get('.Select__menu').contains('Published').click(); + cy.get('.table-row').should('have.length', 2); + cy.get('.table-row').first().contains('USA Births Names').should('exist'); + cy.get('.Select__control').eq(2).click(); + cy.get('.Select__control').eq(2).type('unpub{enter}'); + cy.get('.table-row').should('have.length', 2); + }); +}); diff --git a/superset-frontend/cypress-base/cypress/integration/dashboard_list/list_view.test.ts b/superset-frontend/cypress-base/cypress/integration/dashboard_list/list_view.test.ts index 380a9cee5e..5afe356de3 100644 --- a/superset-frontend/cypress-base/cypress/integration/dashboard_list/list_view.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/dashboard_list/list_view.test.ts @@ -31,17 +31,22 @@ describe('dashboard list view', () => { cy.get('table[role="table"]').should('be.visible'); // check dashboard list view header cy.get('th[role="columnheader"]:nth-child(2)').contains('Title'); - cy.get('th[role="columnheader"]:nth-child(3)').contains('Owners'); - cy.get('th[role="columnheader"]:nth-child(4)').contains('Modified By'); - cy.get('th[role="columnheader"]:nth-child(5)').contains('Published'); - cy.get('th[role="columnheader"]:nth-child(6)').contains('Modified'); - cy.get('th[role="columnheader"]:nth-child(7)').contains('Actions'); + cy.get('th[role="columnheader"]:nth-child(3)').contains('Modified By'); + cy.get('th[role="columnheader"]:nth-child(4)').contains('Published'); + cy.get('th[role="columnheader"]:nth-child(5)').contains('Modified'); + cy.get('th[role="columnheader"]:nth-child(6)').contains('Created By'); + cy.get('th[role="columnheader"]:nth-child(7)').contains('Owners'); + cy.get('th[role="columnheader"]:nth-child(8)').contains('Actions'); cy.get('.table-row').should('have.length', 4); }); it('should sort correctly', () => { cy.get('th[role="columnheader"]:nth-child(2)').click(); cy.get('.table-row td:nth-child(2):eq(0)').contains('Tabbed Dashboard'); + cy.get('th[role="columnheader"]:nth-child(3)').click(); + cy.get('.table-row td:nth-child(2):eq(0)').contains('Tabbed Dashboard'); + cy.get('th[role="columnheader"]:nth-child(5)').click(); + cy.get('.table-row td:nth-child(2):eq(0)').contains("World Bank's Data"); cy.get('th[role="columnheader"]:nth-child(6)').click(); cy.get('.table-row td:nth-child(2):eq(0)').contains("World Bank's Data"); }); diff --git a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx index 5de21354c2..82211c4cae 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/DashboardBuilder_spec.jsx @@ -62,6 +62,9 @@ describe('DashboardBuilder', () => { dashboardLayout, deleteTopLevelTabs() {}, editMode: false, + showBuilderPane() {}, + setColorSchemeAndUnsavedChanges() {}, + colorScheme: undefined, handleComponentDrop() {}, setDirectPathToChild: sinon.spy(), }; diff --git a/superset-frontend/spec/javascripts/explore/components/CheckboxControl_spec.jsx b/superset-frontend/spec/javascripts/explore/components/CheckboxControl_spec.jsx index 9dc24811f6..9a0740cb01 100644 --- a/superset-frontend/spec/javascripts/explore/components/CheckboxControl_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/CheckboxControl_spec.jsx @@ -21,8 +21,6 @@ import React from 'react'; import sinon from 'sinon'; import { shallow, mount } from 'enzyme'; -import { supersetTheme, ThemeProvider } from '@superset-ui/core'; - import CheckboxControl from 'src/explore/components/controls/CheckboxControl'; import ControlHeader from 'src/explore/components/ControlHeader'; import Checkbox from 'src/components/Checkbox'; @@ -50,10 +48,7 @@ describe('CheckboxControl', () => { }); it('Checks the box when the label is clicked', () => { - const fullComponent = mount(, { - wrappingComponent: ThemeProvider, - wrappingComponentProps: { theme: supersetTheme }, - }); + const fullComponent = mount(); const spy = sinon.spy(fullComponent.instance(), 'onChange'); diff --git a/superset-frontend/src/CRUD/Field.jsx b/superset-frontend/src/CRUD/Field.jsx index 3ed439f2ed..3d56019fa5 100644 --- a/superset-frontend/src/CRUD/Field.jsx +++ b/superset-frontend/src/CRUD/Field.jsx @@ -39,9 +39,10 @@ const propTypes = { compact: PropTypes.bool, }; const defaultProps = { + controlProps: {}, onChange: () => {}, compact: false, - description: null, + desc: null, }; export default class Field extends React.PureComponent { diff --git a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton.jsx b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton.jsx index 3c3e2036cd..0fac3d1159 100644 --- a/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton.jsx +++ b/superset-frontend/src/SqlLab/components/ExploreCtasResultsButton.jsx @@ -37,6 +37,10 @@ const propTypes = { templateParams: PropTypes.string, }; +const defaultProps = { + vizRequest: {}, +}; + class ExploreCtasResultsButton extends React.PureComponent { constructor(props) { super(props); @@ -109,6 +113,7 @@ class ExploreCtasResultsButton extends React.PureComponent { } } ExploreCtasResultsButton.propTypes = propTypes; +ExploreCtasResultsButton.defaultProps = defaultProps; function mapStateToProps({ sqlLab, common }) { return { diff --git a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx index 66a538584b..552ba4ce6b 100644 --- a/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx +++ b/superset-frontend/src/SqlLab/components/SqlEditorLeftBar.jsx @@ -25,7 +25,7 @@ import TableSelector from '../../components/TableSelector'; const propTypes = { queryEditor: PropTypes.object.isRequired, - height: PropTypes.number, + height: PropTypes.number.isRequired, tables: PropTypes.array, actions: PropTypes.object, database: PropTypes.object, diff --git a/superset-frontend/src/SqlLab/components/TemplateParamsEditor.jsx b/superset-frontend/src/SqlLab/components/TemplateParamsEditor.jsx index 4552576881..a15f5638b5 100644 --- a/superset-frontend/src/SqlLab/components/TemplateParamsEditor.jsx +++ b/superset-frontend/src/SqlLab/components/TemplateParamsEditor.jsx @@ -33,6 +33,8 @@ const propTypes = { }; const defaultProps = { + label: null, + description: null, onChange: () => {}, code: '{}', }; diff --git a/superset-frontend/src/components/Checkbox/index.tsx b/superset-frontend/src/components/Checkbox/index.tsx index 6bff6d8947..621ce1755c 100644 --- a/superset-frontend/src/components/Checkbox/index.tsx +++ b/superset-frontend/src/components/Checkbox/index.tsx @@ -30,14 +30,8 @@ interface CheckboxProps { } const Styles = styled.span` - cursor: pointer; - &.primary { - color: ${({ theme }) => theme.colors.primary.base}; - } - &.grayscale { - color: ${({ theme }) => theme.colors.grayscale.light1}; - } - svg { + &, + & svg { vertical-align: top; } `; @@ -45,7 +39,6 @@ const Styles = styled.span` export default function Checkbox({ checked, onChange, style }: CheckboxProps) { return ( { onChange(!checked); diff --git a/superset-frontend/src/components/CheckboxIcons.tsx b/superset-frontend/src/components/CheckboxIcons.tsx index a9addf8159..2c94c863fb 100644 --- a/superset-frontend/src/components/CheckboxIcons.tsx +++ b/superset-frontend/src/components/CheckboxIcons.tsx @@ -28,7 +28,7 @@ export const CheckboxChecked = () => ( > @@ -44,7 +44,7 @@ export const CheckboxHalfChecked = () => ( > @@ -60,7 +60,7 @@ export const CheckboxUnchecked = () => ( > diff --git a/superset-frontend/src/components/Hotkeys.jsx b/superset-frontend/src/components/Hotkeys.jsx index 546d3ed07b..c0d4707bf3 100644 --- a/superset-frontend/src/components/Hotkeys.jsx +++ b/superset-frontend/src/components/Hotkeys.jsx @@ -33,6 +33,10 @@ const propTypes = { placement: PropTypes.string, }; +const defaultProps = { + hotkeys: [], +}; + export default class Hotkeys extends React.PureComponent { componentDidMount() { this.props.hotkeys.forEach(keyConfig => { @@ -82,3 +86,4 @@ export default class Hotkeys extends React.PureComponent { } Hotkeys.propTypes = propTypes; +Hotkeys.defaultProps = defaultProps; diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts index 2efb5f0a18..634631a183 100644 --- a/superset-frontend/src/components/ListView/types.ts +++ b/superset-frontend/src/components/ListView/types.ts @@ -53,7 +53,8 @@ export interface Filter { | 'rel_m_m' | 'rel_o_m' | 'title_or_slug' - | 'name_or_description'; + | 'name_or_description' + | 'all_text'; input?: 'text' | 'textarea' | 'select' | 'checkbox' | 'search'; unfilteredLabel?: string; selects?: SelectOption[]; diff --git a/superset-frontend/src/components/Select/OnPasteSelect.jsx b/superset-frontend/src/components/Select/OnPasteSelect.jsx index e7d08aea0a..fcf1e10343 100644 --- a/superset-frontend/src/components/Select/OnPasteSelect.jsx +++ b/superset-frontend/src/components/Select/OnPasteSelect.jsx @@ -81,12 +81,12 @@ export default class OnPasteSelect extends React.Component { } OnPasteSelect.propTypes = { - separator: PropTypes.array, + separator: PropTypes.array.isRequired, selectWrap: PropTypes.elementType, selectRef: PropTypes.func, onChange: PropTypes.func.isRequired, - valueKey: PropTypes.string, - labelKey: PropTypes.string, + valueKey: PropTypes.string.isRequired, + labelKey: PropTypes.string.isRequired, options: PropTypes.array, isMulti: PropTypes.bool, value: PropTypes.any, diff --git a/superset-frontend/src/dashboard/components/ColorSchemeControlWrapper.jsx b/superset-frontend/src/dashboard/components/ColorSchemeControlWrapper.jsx index d60794640f..b2e14d1037 100644 --- a/superset-frontend/src/dashboard/components/ColorSchemeControlWrapper.jsx +++ b/superset-frontend/src/dashboard/components/ColorSchemeControlWrapper.jsx @@ -24,13 +24,13 @@ import { getCategoricalSchemeRegistry, t } from '@superset-ui/core'; import ColorSchemeControl from 'src/explore/components/controls/ColorSchemeControl'; const propTypes = { - onChange: PropTypes.func, + onChange: PropTypes.func.isRequired, colorScheme: PropTypes.string, }; const defaultProps = { - onChange: () => {}, colorScheme: undefined, + onChange: () => {}, }; class ColorSchemeControlWrapper extends React.PureComponent { diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx index 3f798c1106..ecaf3cf2ea 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx @@ -54,6 +54,9 @@ const propTypes = { dashboardLayout: PropTypes.object.isRequired, deleteTopLevelTabs: PropTypes.func.isRequired, editMode: PropTypes.bool.isRequired, + showBuilderPane: PropTypes.func.isRequired, + colorScheme: PropTypes.string, + setColorSchemeAndUnsavedChanges: PropTypes.func.isRequired, handleComponentDrop: PropTypes.func.isRequired, directPathToChild: PropTypes.arrayOf(PropTypes.string), setDirectPathToChild: PropTypes.func.isRequired, @@ -61,7 +64,9 @@ const propTypes = { }; const defaultProps = { + showBuilderPane: false, directPathToChild: [], + colorScheme: undefined, }; class DashboardBuilder extends React.Component { @@ -150,7 +155,14 @@ class DashboardBuilder extends React.Component { } render() { - const { handleComponentDrop, dashboardLayout, editMode } = this.props; + const { + handleComponentDrop, + dashboardLayout, + editMode, + showBuilderPane, + setColorSchemeAndUnsavedChanges, + colorScheme, + } = this.props; const { tabIndex } = this.state; const dashboardRoot = dashboardLayout[DASHBOARD_ROOT_ID]; const rootChildId = dashboardRoot.children[0]; @@ -260,6 +272,9 @@ class DashboardBuilder extends React.Component { {editMode && ( )} diff --git a/superset-frontend/src/dashboard/components/PropertiesModal.jsx b/superset-frontend/src/dashboard/components/PropertiesModal.jsx index 2637f519e2..57f348282e 100644 --- a/superset-frontend/src/dashboard/components/PropertiesModal.jsx +++ b/superset-frontend/src/dashboard/components/PropertiesModal.jsx @@ -35,7 +35,7 @@ import '../stylesheets/buttons.less'; const propTypes = { dashboardId: PropTypes.number.isRequired, - show: PropTypes.bool, + show: PropTypes.bool.isRequired, onHide: PropTypes.func, colorScheme: PropTypes.object, setColorSchemeAndUnsavedChanges: PropTypes.func, diff --git a/superset-frontend/src/dashboard/components/SliceAdder.jsx b/superset-frontend/src/dashboard/components/SliceAdder.jsx index 80d80cbf40..9034a8105a 100644 --- a/superset-frontend/src/dashboard/components/SliceAdder.jsx +++ b/superset-frontend/src/dashboard/components/SliceAdder.jsx @@ -39,7 +39,7 @@ const propTypes = { lastUpdated: PropTypes.number.isRequired, errorMessage: PropTypes.string, userId: PropTypes.string.isRequired, - selectedSliceIds: PropTypes.arrayOf(PropTypes.number), + selectedSliceIds: PropTypes.arrayOf(PropTypes.number).isRequired, editMode: PropTypes.bool, height: PropTypes.number, }; diff --git a/superset-frontend/src/dashboard/components/SliceHeader.jsx b/superset-frontend/src/dashboard/components/SliceHeader.jsx index 276d36a6dc..941c0ad258 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader.jsx +++ b/superset-frontend/src/dashboard/components/SliceHeader.jsx @@ -53,6 +53,7 @@ const propTypes = { const defaultProps = { innerRef: null, forceRefresh: () => ({}), + removeSlice: () => ({}), updateSliceName: () => ({}), toggleExpandSlice: () => ({}), exploreChart: () => ({}), diff --git a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx b/superset-frontend/src/dashboard/components/gridComponents/Row.jsx index f98cac9373..f9076bcf4e 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Row.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Row.jsx @@ -56,6 +56,10 @@ const propTypes = { updateComponents: PropTypes.func.isRequired, }; +const defaultProps = { + rowHeight: null, +}; + class Row extends React.PureComponent { constructor(props) { super(props); @@ -188,5 +192,6 @@ class Row extends React.PureComponent { } Row.propTypes = propTypes; +Row.defaultProps = defaultProps; export default Row; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx index 1765ebb0af..b765cbf93b 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tabs.jsx @@ -69,6 +69,7 @@ const propTypes = { }; const defaultProps = { + children: null, renderTabContent: true, renderHoverMenu: true, availableColumnCount: 0, diff --git a/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.jsx b/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.jsx index eb43901157..42158dccfe 100644 --- a/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.jsx +++ b/superset-frontend/src/dashboard/components/menu/WithPopoverMenu.jsx @@ -35,6 +35,7 @@ const defaultProps = { children: null, disableClick: false, onChangeFocus: null, + onPressDelete() {}, menuItems: [], isFocused: false, shouldFocus: (event, container) => diff --git a/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx b/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx index 9d06e773aa..dacdb7b810 100644 --- a/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardBuilder.jsx @@ -21,6 +21,7 @@ import { connect } from 'react-redux'; import DashboardBuilder from '../components/DashboardBuilder'; import { + setColorSchemeAndUnsavedChanges, showBuilderPane, setDirectPathToChild, setMountedTab, @@ -34,7 +35,9 @@ function mapStateToProps({ dashboardLayout: undoableLayout, dashboardState }) { return { dashboardLayout: undoableLayout.present, editMode: dashboardState.editMode, + showBuilderPane: dashboardState.showBuilderPane, directPathToChild: dashboardState.directPathToChild, + colorScheme: dashboardState.colorScheme, }; } @@ -44,6 +47,7 @@ function mapDispatchToProps(dispatch) { deleteTopLevelTabs, handleComponentDrop, showBuilderPane, + setColorSchemeAndUnsavedChanges, setDirectPathToChild, setMountedTab, }, diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx index aaa7da3ec0..d4a403f752 100644 --- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx @@ -53,6 +53,7 @@ const propTypes = { const defaultProps = { directPathToChild: [], directPathLastUpdated: 0, + isComponentVisible: true, }; function mapStateToProps( diff --git a/superset-frontend/src/explore/components/QueryAndSaveBtns.jsx b/superset-frontend/src/explore/components/QueryAndSaveBtns.jsx index d04cf9d8c4..e169dc4540 100644 --- a/superset-frontend/src/explore/components/QueryAndSaveBtns.jsx +++ b/superset-frontend/src/explore/components/QueryAndSaveBtns.jsx @@ -37,6 +37,7 @@ const propTypes = { const defaultProps = { onStop: () => {}, onSave: () => {}, + disabled: false, }; // Prolly need to move this to a global context diff --git a/superset-frontend/src/explore/components/controls/ColorSchemeControl.jsx b/superset-frontend/src/explore/components/controls/ColorSchemeControl.jsx index 0d3a56e6fd..4b9d6e4f45 100644 --- a/superset-frontend/src/explore/components/controls/ColorSchemeControl.jsx +++ b/superset-frontend/src/explore/components/controls/ColorSchemeControl.jsx @@ -35,8 +35,8 @@ const propTypes = { choices: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.array), PropTypes.func, - ]), - schemes: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), + ]).isRequired, + schemes: PropTypes.oneOfType([PropTypes.object, PropTypes.func]).isRequired, isLinear: PropTypes.bool, }; diff --git a/superset-frontend/src/explore/components/controls/ViewportControl.jsx b/superset-frontend/src/explore/components/controls/ViewportControl.jsx index 11731b14c6..9aa6be1663 100644 --- a/superset-frontend/src/explore/components/controls/ViewportControl.jsx +++ b/superset-frontend/src/explore/components/controls/ViewportControl.jsx @@ -37,7 +37,7 @@ export const DEFAULT_VIEWPORT = { const PARAMS = ['longitude', 'latitude', 'zoom', 'bearing', 'pitch']; const propTypes = { - onChange: PropTypes.func, + onChange: PropTypes.func.isRequired, value: PropTypes.shape({ longitude: PropTypes.number, latitude: PropTypes.number, diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx index 4efdd9ff87..983e14c818 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx @@ -55,6 +55,7 @@ interface Dashboard { url: string; thumbnail_url: string; owners: Owner[]; + created_by: object; } function DashboardList(props: DashboardListProps) { @@ -195,24 +196,7 @@ function DashboardList(props: DashboardListProps) { Header: t('Title'), accessor: 'dashboard_title', }, - { - Cell: ({ - row: { - original: { owners }, - }, - }: any) => ( - - `${firstName} ${lastName}`, - )} - display={2} - /> - ), - Header: t('Owners'), - accessor: 'owners', - disableSortBy: true, - }, + { Cell: ({ row: { @@ -252,6 +236,35 @@ function DashboardList(props: DashboardListProps) { hidden: true, disableSortBy: true, }, + { + Cell: ({ + row: { + original: { created_by: createdBy }, + }, + }: any) => + createdBy ? `${createdBy.first_name} ${createdBy.last_name}` : '', + Header: t('Created By'), + accessor: 'created_by', + disableSortBy: true, + }, + { + Cell: ({ + row: { + original: { owners }, + }, + }: any) => ( + + `${firstName} ${lastName}`, + )} + display={2} + /> + ), + Header: t('Owners'), + accessor: 'owners', + disableSortBy: true, + }, { Cell: ({ row: { original } }: any) => { const handleDelete = () => handleDashboardDelete(original); @@ -329,7 +342,27 @@ function DashboardList(props: DashboardListProps) { createErrorHandler(errMsg => props.addDangerToast( t( - 'An error occurred while fetching chart owner values: %s', + 'An error occurred while fetching dashboard owner values: %s', + errMsg, + ), + ), + ), + ), + paginate: true, + }, + { + Header: t('Created By'), + id: 'created_by', + input: 'select', + operator: 'rel_o_m', + unfilteredLabel: 'All', + fetchSelects: createFetchRelated( + 'dashboard', + 'created_by', + createErrorHandler(errMsg => + props.addDangerToast( + t( + 'An error occurred while fetching dashboard created by values: %s', errMsg, ), ), diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index d691316836..c9c1fc3e24 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -113,6 +113,9 @@ class DashboardRestApi(BaseSupersetModelRestApi): "changed_by_url", "changed_on_utc", "changed_on_delta_humanized", + "created_by.first_name", + "created_by.id", + "created_by.last_name", "dashboard_title", "owners.id", "owners.username", @@ -121,10 +124,11 @@ class DashboardRestApi(BaseSupersetModelRestApi): ] list_select_columns = list_columns + ["changed_on", "changed_by_fk"] order_columns = [ - "dashboard_title", + "changed_by.first_name", "changed_on_delta_humanized", + "created_by.first_name", + "dashboard_title", "published", - "changed_by.first_name", ] add_columns = [ @@ -138,7 +142,7 @@ class DashboardRestApi(BaseSupersetModelRestApi): ] edit_columns = add_columns - search_columns = ("dashboard_title", "slug", "owners", "published") + search_columns = ("dashboard_title", "slug", "owners", "published", "created_by") search_filters = {"dashboard_title": [DashboardTitleOrSlugFilter]} base_order = ("changed_on", "desc") @@ -152,9 +156,10 @@ class DashboardRestApi(BaseSupersetModelRestApi): "owners": ("first_name", "asc"), } related_field_filters = { - "owners": RelatedFieldFilter("first_name", FilterRelatedOwners) + "owners": RelatedFieldFilter("first_name", FilterRelatedOwners), + "created_by": RelatedFieldFilter("first_name", FilterRelatedOwners), } - allowed_rel_fields = {"owners"} + allowed_rel_fields = {"owners", "created_by"} openapi_spec_tag = "Dashboards" apispec_parameter_schemas = { diff --git a/superset/models/dashboard.py b/superset/models/dashboard.py index 23b2a047cf..86484791fc 100644 --- a/superset/models/dashboard.py +++ b/superset/models/dashboard.py @@ -55,7 +55,6 @@ from superset.utils.urls import get_url_path if TYPE_CHECKING: - # pylint: disable=unused-import from superset.connectors.base.models import BaseDatasource metadata = Model.metadata # pylint: disable=no-member @@ -255,8 +254,11 @@ def position(self) -> Dict[str, Any]: return {} @classmethod - def import_obj( # pylint: disable=too-many-locals,too-many-branches,too-many-statements - cls, dashboard_to_import: "Dashboard", import_time: Optional[int] = None, + def import_obj( + # pylint: disable=too-many-locals,too-many-branches,too-many-statements + cls, + dashboard_to_import: "Dashboard", + import_time: Optional[int] = None, ) -> int: """Imports the dashboard from the object to the database. diff --git a/superset/models/datasource_access_request.py b/superset/models/datasource_access_request.py index 974633294b..98362ec316 100644 --- a/superset/models/datasource_access_request.py +++ b/superset/models/datasource_access_request.py @@ -26,9 +26,7 @@ from superset.utils import core as utils if TYPE_CHECKING: - from superset.connectors.base.models import ( # pylint: disable=unused-import - BaseDatasource, - ) + from superset.connectors.base.models import BaseDatasource config = app.config @@ -74,7 +72,6 @@ def roles_with_datasource(self) -> str: for role in pv.role: if role.name in self.ROLES_DENYLIST: continue - # pylint: disable=no-member href = ( f"/superset/approve?datasource_type={self.datasource_type}&" f"datasource_id={self.datasource_id}&" @@ -87,8 +84,7 @@ def roles_with_datasource(self) -> str: @property def user_roles(self) -> str: action_list = "" - for role in self.created_by.roles: # pylint: disable=no-member - # pylint: disable=no-member + for role in self.created_by.roles: href = ( f"/superset/approve?datasource_type={self.datasource_type}&" f"datasource_id={self.datasource_id}&" diff --git a/superset/models/helpers.py b/superset/models/helpers.py index fd55bba5ce..dd3a068d94 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -136,7 +136,7 @@ def import_from_dict( parent: Optional[Any] = None, recursive: bool = True, sync: Optional[List[str]] = None, - ) -> Any: # pylint: disable=too-many-arguments,too-many-locals,too-many-branches + ) -> Any: """Import obj from a dictionary""" if sync is None: sync = [] diff --git a/superset/models/slice.py b/superset/models/slice.py index f9ec88e5fc..a238e194e4 100644 --- a/superset/models/slice.py +++ b/superset/models/slice.py @@ -43,7 +43,6 @@ from superset.viz import BaseViz, viz_types # type: ignore if TYPE_CHECKING: - # pylint: disable=unused-import from superset.connectors.base.models import BaseDatasource metadata = Model.metadata # pylint: disable=no-member @@ -346,8 +345,7 @@ def url(self) -> str: return f"/superset/explore/?form_data=%7B%22slice_id%22%3A%20{self.id}%7D" -def set_related_perm(mapper: Mapper, connection: Connection, target: Slice) -> None: - # pylint: disable=unused-argument +def set_related_perm(_mapper: Mapper, _connection: Connection, target: Slice) -> None: src_class = target.cls_model id_ = target.datasource_id if id_: @@ -357,8 +355,8 @@ def set_related_perm(mapper: Mapper, connection: Connection, target: Slice) -> N target.schema_perm = ds.schema_perm -def event_after_chart_changed( # pylint: disable=unused-argument - mapper: Mapper, connection: Connection, target: Slice +def event_after_chart_changed( + _mapper: Mapper, _connection: Connection, target: Slice ) -> None: url = get_url_path("Superset.slice", slice_id=target.id, standalone="true") cache_chart_thumbnail.delay(url, target.digest, force=True) diff --git a/superset/models/tags.py b/superset/models/tags.py index c09bb16858..3f508ff6c6 100644 --- a/superset/models/tags.py +++ b/superset/models/tags.py @@ -29,10 +29,10 @@ from superset.models.helpers import AuditMixinNullable if TYPE_CHECKING: - from superset.models.core import FavStar # pylint: disable=unused-import - from superset.models.dashboard import Dashboard # pylint: disable=unused-import - from superset.models.slice import Slice # pylint: disable=unused-import - from superset.models.sql_lab import Query # pylint: disable=unused-import + from superset.models.core import FavStar + from superset.models.dashboard import Dashboard + from superset.models.slice import Slice + from superset.models.sql_lab import Query Session = sessionmaker(autoflush=False) @@ -136,11 +136,10 @@ def _add_owners( @classmethod def after_insert( cls, - mapper: Mapper, + _mapper: Mapper, connection: Connection, target: Union["Dashboard", "FavStar", "Slice"], ) -> None: - # pylint: disable=unused-argument session = Session(bind=connection) # add `owner:` tags @@ -158,11 +157,10 @@ def after_insert( @classmethod def after_update( cls, - mapper: Mapper, + _mapper: Mapper, connection: Connection, target: Union["Dashboard", "FavStar", "Slice"], ) -> None: - # pylint: disable=unused-argument session = Session(bind=connection) # delete current `owner:` tags @@ -188,11 +186,10 @@ def after_update( @classmethod def after_delete( cls, - mapper: Mapper, + _mapper: Mapper, connection: Connection, target: Union["Dashboard", "FavStar", "Slice"], ) -> None: - # pylint: disable=unused-argument session = Session(bind=connection) # delete row from `tagged_objects` @@ -234,9 +231,8 @@ def get_owners_ids(cls, target: "Query") -> List[int]: class FavStarUpdater: @classmethod def after_insert( - cls, mapper: Mapper, connection: Connection, target: "FavStar" + cls, _mapper: Mapper, connection: Connection, target: "FavStar" ) -> None: - # pylint: disable=unused-argument session = Session(bind=connection) name = "favorited_by:{0}".format(target.user_id) tag = get_tag(name, session, TagTypes.favorited_by) @@ -251,9 +247,8 @@ def after_insert( @classmethod def after_delete( - cls, mapper: Mapper, connection: Connection, target: "FavStar" + cls, _mapper: Mapper, connection: Connection, target: "FavStar" ) -> None: - # pylint: disable=unused-argument session = Session(bind=connection) name = "favorited_by:{0}".format(target.user_id) query = ( diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py index 81204a8b1c..af0dcd1c8d 100644 --- a/superset/queries/saved_queries/api.py +++ b/superset/queries/saved_queries/api.py @@ -32,7 +32,10 @@ SavedQueryBulkDeleteFailedError, SavedQueryNotFoundError, ) -from superset.queries.saved_queries.filters import SavedQueryFilter +from superset.queries.saved_queries.filters import ( + SavedQueryAllTextFilter, + SavedQueryFilter, +) from superset.queries.saved_queries.schemas import ( get_delete_ids_schema, openapi_spec_methods_override, @@ -93,6 +96,8 @@ class SavedQueryRestApi(BaseSupersetModelRestApi): "database.database_name", ] + search_filters = {"label": [SavedQueryAllTextFilter]} + apispec_parameter_schemas = { "get_delete_ids_schema": get_delete_ids_schema, } @@ -117,9 +122,7 @@ def pre_update(self, item: SavedQuery) -> None: @safe @statsd_metrics @rison(get_delete_ids_schema) - def bulk_delete( - self, **kwargs: Any - ) -> Response: # pylint: disable=arguments-differ + def bulk_delete(self, **kwargs: Any) -> Response: """Delete bulk Saved Queries --- delete: diff --git a/superset/queries/saved_queries/filters.py b/superset/queries/saved_queries/filters.py index 498a061edc..09636cc3a8 100644 --- a/superset/queries/saved_queries/filters.py +++ b/superset/queries/saved_queries/filters.py @@ -17,12 +17,33 @@ from typing import Any from flask import g +from flask_babel import lazy_gettext as _ from flask_sqlalchemy import BaseQuery +from sqlalchemy import or_ +from sqlalchemy.orm.query import Query from superset.models.sql_lab import SavedQuery from superset.views.base import BaseFilter +class SavedQueryAllTextFilter(BaseFilter): # pylint: disable=too-few-public-methods + name = _("All Text") + arg_name = "all_text" + + def apply(self, query: Query, value: Any) -> Query: + if not value: + return query + ilike_value = f"%{value}%" + return query.filter( + or_( + SavedQuery.schema.ilike(ilike_value), + SavedQuery.label.ilike(ilike_value), + SavedQuery.description.ilike(ilike_value), + SavedQuery.sql.ilike(ilike_value), + ) + ) + + class SavedQueryFilter(BaseFilter): # pylint: disable=too-few-public-methods def apply(self, query: BaseQuery, value: Any) -> BaseQuery: """ diff --git a/superset/security/manager.py b/superset/security/manager.py index 858ecdc157..df2b3e0f8e 100644 --- a/superset/security/manager.py +++ b/superset/security/manager.py @@ -990,9 +990,7 @@ def raise_for_access( # pylint: disable=too-many-arguments,too-many-branches self.get_datasource_access_error_object(datasource) ) - def get_rls_filters( # pylint: disable=no-self-use - self, table: "BaseDatasource" - ) -> List[SqlaQuery]: + def get_rls_filters(self, table: "BaseDatasource") -> List[SqlaQuery]: """ Retrieves the appropriate row level security filters for the current user and the passed table. diff --git a/superset/sql_validators/presto_db.py b/superset/sql_validators/presto_db.py index ccafb98474..e2de531f7d 100644 --- a/superset/sql_validators/presto_db.py +++ b/superset/sql_validators/presto_db.py @@ -53,10 +53,9 @@ def validate_statement( sql = parsed_query.stripped() # Hook to allow environment-specific mutation (usually comments) to the SQL - # pylint: disable=invalid-name - SQL_QUERY_MUTATOR = config["SQL_QUERY_MUTATOR"] - if SQL_QUERY_MUTATOR: - sql = SQL_QUERY_MUTATOR(sql, user_name, security_manager, database) + sql_query_mutator = config["SQL_QUERY_MUTATOR"] + if sql_query_mutator: + sql = sql_query_mutator(sql, user_name, security_manager, database) # Transform the final statement to an explain call before sending it on # to presto to validate diff --git a/superset/tasks/alerts/observer.py b/superset/tasks/alerts/observer.py index 482faedd80..b6805cf65e 100644 --- a/superset/tasks/alerts/observer.py +++ b/superset/tasks/alerts/observer.py @@ -22,8 +22,8 @@ import pandas as pd from sqlalchemy.orm import Session +from superset import jinja_context from superset.models.alerts import Alert, SQLObservation -from superset.sql_parse import ParsedQuery logger = logging.getLogger("tasks.email_reports") @@ -42,9 +42,9 @@ def observe(alert_id: int, session: Session) -> Optional[str]: value = None - parsed_query = ParsedQuery(sql_observer.sql) - sql = parsed_query.stripped() - df = sql_observer.database.get_df(sql) + tp = jinja_context.get_template_processor(database=sql_observer.database) + rendered_sql = tp.process_template(sql_observer.sql) + df = sql_observer.database.get_df(rendered_sql) error_msg = validate_observer_result(df, alert.id, alert.label) diff --git a/tests/alerts_tests.py b/tests/alerts_tests.py index b09ac930e7..53245e69cb 100644 --- a/tests/alerts_tests.py +++ b/tests/alerts_tests.py @@ -154,6 +154,27 @@ def test_alert_observer(setup_database): assert alert7.sql_observer[0].observations[-1].value is None assert alert7.sql_observer[0].observations[-1].error_msg is not None + # Test multiline SQLObserver + alert8 = create_alert( + dbsession, + """ + -- comment + SELECT + 1 -- comment + FROM test_table + WHERE first = 1 + """, + ) + observe(alert8.id, dbsession) + assert alert8.sql_observer[0].observations[-1].value == 1.0 + assert alert8.sql_observer[0].observations[-1].error_msg is None + + # Test jinja + alert9 = create_alert(dbsession, "SELECT {{ 2 }}") + observe(alert9.id, dbsession) + assert alert9.sql_observer[0].observations[-1].value == 2.0 + assert alert9.sql_observer[0].observations[-1].error_msg is None + @patch("superset.tasks.schedules.deliver_alert") def test_evaluate_alert(mock_deliver_alert, setup_database): diff --git a/tests/dashboards/api_tests.py b/tests/dashboards/api_tests.py index 6645e802fc..17cffd411b 100644 --- a/tests/dashboards/api_tests.py +++ b/tests/dashboards/api_tests.py @@ -51,6 +51,7 @@ def insert_dashboard( dashboard_title: str, slug: Optional[str], owners: List[int], + created_by=None, slices: Optional[List[Slice]] = None, position_json: str = "", css: str = "", @@ -71,6 +72,7 @@ def insert_dashboard( json_metadata=json_metadata, slices=slices, published=published, + created_by=created_by, ) db.session.add(dashboard) db.session.commit() @@ -81,7 +83,7 @@ def test_get_dashboard(self): Dashboard API: Test get dashboard """ admin = self.get_user("admin") - dashboard = self.insert_dashboard("title", "slug1", [admin.id]) + dashboard = self.insert_dashboard("title", "slug1", [admin.id], admin) self.login(username="admin") uri = f"api/v1/dashboard/{dashboard.id}" rv = self.get_assert_metric(uri, "get") @@ -91,6 +93,7 @@ def test_get_dashboard(self): "changed_by_name": "", "changed_by_url": "", "charts": [], + "created_by": {"id": 1, "first_name": "admin", "last_name": "user",}, "id": dashboard.id, "css": "", "dashboard_title": "title", diff --git a/tests/queries/saved_queries/api_tests.py b/tests/queries/saved_queries/api_tests.py index b3ce625b1f..f268b1dc06 100644 --- a/tests/queries/saved_queries/api_tests.py +++ b/tests/queries/saved_queries/api_tests.py @@ -43,6 +43,7 @@ def insert_saved_query( db_id: Optional[int] = None, created_by=None, schema: Optional[str] = "", + description: Optional[str] = "", ) -> SavedQuery: database = None if db_id: @@ -53,6 +54,7 @@ def insert_saved_query( sql=sql, label=label, schema=schema, + description=description, ) db.session.add(query) db.session.commit() @@ -69,6 +71,7 @@ def insert_default_saved_query( db_id=example_db.id, created_by=admin, schema=schema, + description="cool description", ) @pytest.fixture() @@ -195,6 +198,95 @@ def test_get_list_filter_saved_query(self): data = json.loads(rv.data.decode("utf-8")) assert data["count"] == len(all_queries) + @pytest.mark.usefixtures("create_saved_queries") + def test_get_list_custom_filter_schema_saved_query(self): + """ + Saved Query API: Test get list and custom filter (schema) saved query + """ + self.login(username="admin") + admin = self.get_user("admin") + + all_queries = ( + db.session.query(SavedQuery) + .filter(SavedQuery.created_by == admin) + .filter(SavedQuery.schema.ilike("%2%")) + .all() + ) + query_string = { + "filters": [{"col": "label", "opr": "all_text", "value": "schema2"}], + } + uri = f"api/v1/saved_query/?q={prison.dumps(query_string)}" + rv = self.get_assert_metric(uri, "get_list") + assert rv.status_code == 200 + data = json.loads(rv.data.decode("utf-8")) + assert data["count"] == len(all_queries) + + @pytest.mark.usefixtures("create_saved_queries") + def test_get_list_custom_filter_label_saved_query(self): + """ + Saved Query API: Test get list and custom filter (label) saved query + """ + self.login(username="admin") + admin = self.get_user("admin") + all_queries = ( + db.session.query(SavedQuery) + .filter(SavedQuery.created_by == admin) + .filter(SavedQuery.label.ilike("%3%")) + .all() + ) + query_string = { + "filters": [{"col": "label", "opr": "all_text", "value": "label3"}], + } + uri = f"api/v1/saved_query/?q={prison.dumps(query_string)}" + rv = self.get_assert_metric(uri, "get_list") + assert rv.status_code == 200 + data = json.loads(rv.data.decode("utf-8")) + assert data["count"] == len(all_queries) + + @pytest.mark.usefixtures("create_saved_queries") + def test_get_list_custom_filter_sql_saved_query(self): + """ + Saved Query API: Test get list and custom filter (sql) saved query + """ + self.login(username="admin") + admin = self.get_user("admin") + all_queries = ( + db.session.query(SavedQuery) + .filter(SavedQuery.created_by == admin) + .filter(SavedQuery.sql.ilike("%table%")) + .all() + ) + query_string = { + "filters": [{"col": "label", "opr": "all_text", "value": "table"}], + } + uri = f"api/v1/saved_query/?q={prison.dumps(query_string)}" + rv = self.get_assert_metric(uri, "get_list") + assert rv.status_code == 200 + data = json.loads(rv.data.decode("utf-8")) + assert data["count"] == len(all_queries) + + @pytest.mark.usefixtures("create_saved_queries") + def test_get_list_custom_filter_description_saved_query(self): + """ + Saved Query API: Test get list and custom filter (description) saved query + """ + self.login(username="admin") + admin = self.get_user("admin") + all_queries = ( + db.session.query(SavedQuery) + .filter(SavedQuery.created_by == admin) + .filter(SavedQuery.description.ilike("%cool%")) + .all() + ) + query_string = { + "filters": [{"col": "label", "opr": "all_text", "value": "cool"}], + } + uri = f"api/v1/saved_query/?q={prison.dumps(query_string)}" + rv = self.get_assert_metric(uri, "get_list") + assert rv.status_code == 200 + data = json.loads(rv.data.decode("utf-8")) + assert data["count"] == len(all_queries) + def test_info_saved_query(self): """ SavedQuery API: Test info @@ -281,7 +373,7 @@ def test_get_saved_query(self): expected_result = { "id": saved_query.id, "database": {"id": saved_query.database.id, "database_name": "examples"}, - "description": None, + "description": "cool description", "created_by": { "first_name": saved_query.created_by.first_name, "id": saved_query.created_by.id,