diff --git a/superset-frontend/spec/javascripts/components/SupersetResourceSelect_spec.tsx b/superset-frontend/spec/javascripts/components/SupersetResourceSelect_spec.tsx index 0d5859de1af..28509bfe4d7 100644 --- a/superset-frontend/spec/javascripts/components/SupersetResourceSelect_spec.tsx +++ b/superset-frontend/spec/javascripts/components/SupersetResourceSelect_spec.tsx @@ -22,6 +22,16 @@ import SupersetResourceSelect from 'src/component/SupersetResourceSelect'; describe('SupersetREsourceSelect', () => { it('is a valid element', () => { - expect(React.isValidElement()).toBe(true); + }); + it('take in props', () => { + const selectProps = { + resource: 'dataset', + searchColumn: 'table_name', + transformItem: jest.fn(), + isMulti: false, + }; + const wrapper = mount(); + expect(wrapper.props().props.resource).toEqual('dataset'); + }); +}); diff --git a/superset-frontend/src/dashboard/actions/nativeFilters.ts b/superset-frontend/src/dashboard/actions/nativeFilters.ts index 2a090fcc591..3a1b7cfc29d 100644 --- a/superset-frontend/src/dashboard/actions/nativeFilters.ts +++ b/superset-frontend/src/dashboard/actions/nativeFilters.ts @@ -19,7 +19,7 @@ import { SupersetClient } from '@superset-ui/core'; import { Dispatch } from 'redux'; -import { Filter } from '../components/filterConfig/types'; +import { Filter } from '../components/nativeFilters/types'; import { dashboardInfoChanged } from './dashboardInfo'; export const CREATE_FILTER_BEGIN = 'CREATE_FILTER_BEGIN'; @@ -97,6 +97,7 @@ export interface SelectFilterOptionAction { filterId: string; selectedValues: string[] | null; } + /** * Sets the selected option(s) for a given filter * @param filterId the id of the native filter diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx index 3772893bf96..7d05c342c06 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder.jsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder.jsx @@ -46,6 +46,7 @@ import { DASHBOARD_ROOT_ID, DASHBOARD_ROOT_DEPTH, } from '../util/constants'; +import FilterBar from './nativeFilters/FilterBar'; const TABS_HEIGHT = 47; const HEADER_HEIGHT = 67; @@ -82,10 +83,13 @@ const StyledDashboardContent = styled.div` padding-left: 0; } - & > div:first-child { + .grid-container { + /* without this, the grid will not get smaller upon toggling the builder panel on */ + min-width: 0; width: 100%; flex-grow: 1; position: relative; + margin: 24px 36px 24px; } .dashboard-component-chart-holder { @@ -251,6 +255,9 @@ class DashboardBuilder extends React.Component { +
+ +
{({ width }) => ( diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header.jsx index 4cc66384b22..2dea03525e8 100644 --- a/superset-frontend/src/dashboard/components/Header.jsx +++ b/superset-frontend/src/dashboard/components/Header.jsx @@ -48,7 +48,7 @@ import { } from '../util/constants'; import setPeriodicRunner from '../util/setPeriodicRunner'; import { options as PeriodicRefreshOptions } from './RefreshIntervalModal'; -import CreateFilterButton from './filterConfig/CreateFilterButton'; +import CreateFilterButton from './nativeFilters/CreateFilterButton'; const propTypes = { addSuccessToast: PropTypes.func.isRequired, @@ -103,6 +103,14 @@ const defaultProps = { // Styled Components const StyledDashboardHeader = styled.div` + background: ${({ theme }) => theme.colors.grayscale.light5}; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 0 ${({ theme }) => theme.gridUnit * 6}px; + border-bottom: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; + button, .fave-unfave-icon { margin-left: ${({ theme }) => theme.gridUnit * 2}px; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx new file mode 100644 index 00000000000..3887e8f8c79 --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar.tsx @@ -0,0 +1,97 @@ +/** + * 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 { styled } from '@superset-ui/core'; +import React from 'react'; +import { Button, Form, Input } from 'src/common/components'; +import { + useFilterConfigurations, + useFilterSetter, + useFilterState, +} from './state'; +import { Filter } from './types'; + +const Bar = styled.div` + display: flex; + flex-direction: column; + width: 250px; // arbitrary... + flex-grow: 1; + padding: ${({ theme }) => theme.gridUnit * 4}px; + background: ${({ theme }) => theme.colors.grayscale.light5}; + border-right: 1px solid ${({ theme }) => theme.colors.grayscale.light2}; +`; + +interface FilterProps { + filter: Filter; +} + +const FilterValue: React.FC = ({ filter }) => { + // THIS ONE IS BUILT TO THROW AWAY + // this is a temporary POC implementation just to get state hooked up. + // Please don't send this component to prod. + const { selectedValues } = useFilterState(filter.id); + const setSelectedValues = useFilterSetter(filter.id); + + if (selectedValues) { + return ( + + {selectedValues.join(', ')} + + + ); + } + return ( +
{ + console.log(values.value); + setSelectedValues(values.value); + }} + > + + + + +
+ ); +}; + +const FilterControl: React.FC = ({ filter }) => { + return ( +
+

{filter.name}

+ +
+ ); +}; + +const FilterBar: React.FC = () => { + const filterConfigs = useFilterConfigurations(); + return ( + + {filterConfigs.map(filter => ( + + ))} + + ); +}; + +export default FilterBar; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/state.ts b/superset-frontend/src/dashboard/components/nativeFilters/state.ts new file mode 100644 index 00000000000..cf46b4c688e --- /dev/null +++ b/superset-frontend/src/dashboard/components/nativeFilters/state.ts @@ -0,0 +1,44 @@ +/** + * 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 { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { selectFilterOption } from 'src/dashboard/actions/nativeFilters'; +import { getInitialFilterState } from 'src/dashboard/reducers/nativeFilters'; +import { Filter, FilterState } from './types'; + +export function useFilterConfigurations() { + return useSelector( + state => state.dashboardInfo.metadata.filter_configuration || [], + ); +} + +export function useFilterState(id: string) { + return useSelector( + state => state.nativeFilters[id] || getInitialFilterState(id), + ); +} + +export function useFilterSetter(id: string) { + const dispatch = useDispatch(); + return useCallback( + (values: string | string[] | null) => + dispatch(selectFilterOption(id, values)), + [id, dispatch], + ); +} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/types.ts b/superset-frontend/src/dashboard/components/nativeFilters/types.ts index 0b28d61c7f4..8b5695a647c 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/types.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/types.ts @@ -34,6 +34,10 @@ export interface Target { export type FilterType = 'text' | 'date'; +/** + * This is a filter configuration object, stored in the dashboard's json metadata. + * The values here do not reflect the current state of the filter. + */ export interface Filter { id: string; // randomly generated at filter creation name: string; @@ -47,3 +51,16 @@ export interface Filter { // maybe someday support this? // displayColumnsInOptions: Column[]; } + +/** Current state of the filter, stored in `nativeFilters` in redux */ +export type FilterState = { + id: string; // ties this filter state to the config object + optionsStatus: 'loading' | 'success' | 'fail'; + options: string[] | null; + selectedValues: string[] | null; + /** + * If the config changes, the current options/values may no longer be valid. + * isDirty indicates that state. + */ + isDirty: boolean; +}; diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 5b2901c502c..5a32c97dd2c 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -22,6 +22,7 @@ import shortid from 'shortid'; import { CategoricalColorNamespace } from '@superset-ui/core'; import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; +import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters'; import { getParam } from 'src/modules/utils'; import { applyDefaultFormData } from 'src/explore/store'; import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters'; @@ -255,7 +256,9 @@ export default function getInitialState(bootstrapData) { directPathToChild.push(directLinkComponentId); } - const nativeFilters = getInitialNativeFilterState(dashboard.metadata); + const nativeFilters = getInitialNativeFilterState( + dashboard.metadata.filter_configuration || [], + ); return { datasources, @@ -279,6 +282,7 @@ export default function getInitialState(bootstrapData) { lastModifiedTime: dashboard.last_modified_time, }, dashboardFilters, + nativeFilters, dashboardState: { sliceIds: Array.from(sliceIds), directPathToChild, diff --git a/superset-frontend/src/dashboard/reducers/index.js b/superset-frontend/src/dashboard/reducers/index.js index afc77ce6c8e..61964de92e6 100644 --- a/superset-frontend/src/dashboard/reducers/index.js +++ b/superset-frontend/src/dashboard/reducers/index.js @@ -22,6 +22,7 @@ import charts from '../../chart/chartReducer'; import dashboardInfo from './dashboardInfo'; import dashboardState from './dashboardState'; import dashboardFilters from './dashboardFilters'; +import nativeFilters from './nativeFilters'; import datasources from './datasources'; import sliceEntities from './sliceEntities'; import dashboardLayout from './undoableDashboardLayout'; @@ -34,6 +35,7 @@ export default combineReducers({ datasources, dashboardInfo, dashboardFilters, + nativeFilters, dashboardState, dashboardLayout, impressionId, diff --git a/superset-frontend/src/dashboard/reducers/nativeFilters.ts b/superset-frontend/src/dashboard/reducers/nativeFilters.ts index f0e5b3aa69d..ef7ca6b6304 100644 --- a/superset-frontend/src/dashboard/reducers/nativeFilters.ts +++ b/superset-frontend/src/dashboard/reducers/nativeFilters.ts @@ -25,21 +25,15 @@ import { SelectFilterOptionAction, SELECT_FILTER_OPTION, } from '../actions/nativeFilters'; -import { Filter } from '../components/nativeFilters/types'; - -export type FilterState = { - optionsStatus: 'loading' | 'success' | 'fail'; - isDirty: boolean; - options: string[] | null; - selectedValues: string[] | null; -}; +import { Filter, FilterState } from '../components/nativeFilters/types'; export type State = { [filterId: string]: FilterState; }; -function getInitialFilterState(): FilterState { +export function getInitialFilterState(id: string): FilterState { return { + id, optionsStatus: 'loading', isDirty: false, options: null, @@ -50,7 +44,7 @@ function getInitialFilterState(): FilterState { export function getInitialState(filterConfig: Filter[]): State { const filters = {}; filterConfig.forEach(filter => { - filters[filter.id] = getInitialFilterState(); + filters[filter.id] = getInitialFilterState(filter.id); }); return filters; } @@ -59,9 +53,10 @@ export default function nativeFilterReducer( filters: State = {}, action: Action, ) { - const reducerMap = { + const actionMap = { [SELECT_FILTER_OPTION]: (action: SelectFilterOptionAction): State => { const filterState = filters[action.filterId]; + console.log('reducer', action, filterState); return { ...filters, [action.filterId]: { @@ -72,7 +67,7 @@ export default function nativeFilterReducer( }, [CREATE_FILTER_BEGIN]: (action: CreateFilterBeginAction): State => { - const filterState = getInitialFilterState(); + const filterState = getInitialFilterState(action.filter.id); filterState.isDirty = true; return { ...filters, @@ -92,8 +87,8 @@ export default function nativeFilterReducer( }, }; - if (reducerMap[action.type]) { - reducerMap[action.type](action); + if (actionMap[action.type]) { + return actionMap[action.type](action); } return filters; } diff --git a/superset-frontend/src/dashboard/stylesheets/builder.less b/superset-frontend/src/dashboard/stylesheets/builder.less index 82b4c59ef0d..ec268601f20 100644 --- a/superset-frontend/src/dashboard/stylesheets/builder.less +++ b/superset-frontend/src/dashboard/stylesheets/builder.less @@ -21,16 +21,6 @@ color: @almost-black; } -.dashboard-header { - background: @lightest; - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - padding: 0 24px; - box-shadow: 0 4px 4px 0 fade(@darkest, @opacity-light); /* @TODO color */ -} - /* only top-level tabs have popover, give it more padding to match header + tabs */ .dashboard > .with-popover-menu > .popover-menu { left: 24px; diff --git a/superset-frontend/src/dashboard/stylesheets/grid.less b/superset-frontend/src/dashboard/stylesheets/grid.less index 03a03af9cad..5b793b96bdb 100644 --- a/superset-frontend/src/dashboard/stylesheets/grid.less +++ b/superset-frontend/src/dashboard/stylesheets/grid.less @@ -16,13 +16,6 @@ * specific language governing permissions and limitations * under the License. */ -.grid-container { - position: relative; - margin: 24px 36px 24px; - /* without this, the grid will not get smaller upon toggling the builder panel on */ - min-width: 0; - width: 100%; -} /* this is the ParentSize wrapper */ .grid-container > div:first-child {