You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ta...@apache.org on 2020/08/14 22:08:04 UTC
[incubator-superset] branch master updated: feat: sort card view by
Alphabetical, Recently Modified, and Least Recently Modified (#10601)
This is an automated email from the ASF dual-hosted git repository.
tai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new 03a62f1 feat: sort card view by Alphabetical, Recently Modified, and Least Recently Modified (#10601)
03a62f1 is described below
commit 03a62f15d80ed323c4647a98e611b13f8eea4f66
Author: Lily Kuang <li...@preset.io>
AuthorDate: Fri Aug 14 15:07:37 2020 -0700
feat: sort card view by Alphabetical, Recently Modified, and Least Recently Modified (#10601)
---
.../components/ListView/ListView_spec.jsx | 53 +++++++++-
.../views/CRUD/chart/ChartList_spec.jsx | 8 +-
.../views/CRUD/dashboard/DashboardList_spec.jsx | 20 +++-
.../src/components/ListView/CardSortSelect.tsx | 114 +++++++++++++++++++++
.../src/components/ListView/Filters.tsx | 49 +++------
.../src/components/ListView/ListView.tsx | 19 +++-
superset-frontend/src/components/ListView/types.ts | 7 ++
superset-frontend/src/components/ListView/utils.ts | 19 +++-
.../src/views/CRUD/chart/ChartList.tsx | 22 ++++
.../src/views/CRUD/dashboard/DashboardList.tsx | 22 ++++
10 files changed, 293 insertions(+), 40 deletions(-)
diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
index 8625f1d..fee729e 100644
--- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
+++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
@@ -22,14 +22,17 @@ import { act } from 'react-dom/test-utils';
import { QueryParamProvider } from 'use-query-params';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
+import Button from 'src/components/Button';
+import CardCollection from 'src/components/ListView/CardCollection';
+import { CardSortSelect } from 'src/components/ListView/CardSortSelect';
+import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import ListView from 'src/components/ListView/ListView';
import ListViewFilters from 'src/components/ListView/Filters';
import ListViewPagination from 'src/components/ListView/Pagination';
import Pagination from 'src/components/Pagination';
-import Button from 'src/components/Button';
+import TableCollection from 'src/components/ListView/TableCollection';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
-import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
function makeMockLocation(query) {
const queryStr = encodeURIComponent(query);
@@ -100,6 +103,14 @@ const mockedProps = {
onSelect: jest.fn(),
},
],
+ cardSortSelectOptions: [
+ {
+ desc: false,
+ id: 'something',
+ label: 'Alphabetical',
+ value: 'alphabetical',
+ },
+ ],
};
const factory = (props = mockedProps) =>
@@ -281,6 +292,24 @@ describe('ListView', () => {
);
});
+ it('disable card view based on prop', async () => {
+ expect(wrapper.find(CardCollection).exists()).toBe(false);
+ expect(wrapper.find(CardSortSelect).exists()).toBe(false);
+ expect(wrapper.find(TableCollection).exists()).toBe(true);
+ });
+
+ it('enable card view based on prop', async () => {
+ const wrapper2 = factory({
+ ...mockedProps,
+ renderCard: jest.fn(),
+ initialSort: [{ id: 'something' }],
+ });
+ await waitForComponentToPaint(wrapper2);
+ expect(wrapper2.find(CardCollection).exists()).toBe(true);
+ expect(wrapper2.find(CardSortSelect).exists()).toBe(true);
+ expect(wrapper2.find(TableCollection).exists()).toBe(false);
+ });
+
it('Throws an exception if filter missing in columns', () => {
expect.assertions(1);
const props = {
@@ -377,4 +406,24 @@ describe('ListView', () => {
]
`);
});
+
+ it('calls fetchData on card view sort', async () => {
+ const wrapper2 = factory({
+ ...mockedProps,
+ renderCard: jest.fn(),
+ initialSort: [{ id: 'something' }],
+ });
+
+ act(() => {
+ wrapper2.find('[data-test="card-sort-select"]').first().props().onChange({
+ desc: false,
+ id: 'something',
+ label: 'Alphabetical',
+ value: 'alphabetical',
+ });
+ });
+
+ wrapper2.update();
+ expect(mockedProps.fetchData).toHaveBeenCalled();
+ });
});
diff --git a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
index 2083b4a..213ac39 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
@@ -24,6 +24,7 @@ import fetchMock from 'fetch-mock';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
import ChartList from 'src/views/CRUD/chart/ChartList';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import ListView from 'src/components/ListView';
import PropertiesModal from 'src/explore/components/PropertiesModal';
import ListViewCard from 'src/components/ListViewCard';
@@ -49,7 +50,7 @@ const mockCharts = [...new Array(3)].map((_, i) => ({
}));
fetchMock.get(chartsInfoEndpoint, {
- permissions: ['can_list', 'can_edit'],
+ permissions: ['can_list', 'can_edit', 'can_delete'],
});
fetchMock.get(chartssOwnersEndpoint, {
result: [],
@@ -113,4 +114,9 @@ describe('ChartList', () => {
wrapper.find('[data-test="pencil"]').first().simulate('click');
expect(wrapper.find(PropertiesModal)).toExist();
});
+
+ it('delete', () => {
+ wrapper.find('[data-test="trash"]').first().simulate('click');
+ expect(wrapper.find(ConfirmStatusChange)).toExist();
+ });
});
diff --git a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
index eef4ca0..37d5ca2 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
@@ -23,10 +23,11 @@ import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
+import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import DashboardList from 'src/views/CRUD/dashboard/DashboardList';
import ListView from 'src/components/ListView';
-import PropertiesModal from 'src/dashboard/components/PropertiesModal';
import ListViewCard from 'src/components/ListViewCard';
+import PropertiesModal from 'src/dashboard/components/PropertiesModal';
// store needed for withToasts(DashboardTable)
const mockStore = configureStore([thunk]);
@@ -50,7 +51,7 @@ const mockDashboards = [...new Array(3)].map((_, i) => ({
}));
fetchMock.get(dashboardsInfoEndpoint, {
- permissions: ['can_list', 'can_edit'],
+ permissions: ['can_list', 'can_edit', 'can_delete'],
});
fetchMock.get(dashboardOwnersEndpoint, {
result: [],
@@ -104,4 +105,19 @@ describe('DashboardList', () => {
wrapper.find('[data-test="pencil"]').first().simulate('click');
expect(wrapper.find(PropertiesModal)).toExist();
});
+
+ it('card view edits', () => {
+ wrapper.find('[data-test="pencil"]').last().simulate('click');
+ expect(wrapper.find(PropertiesModal)).toExist();
+ });
+
+ it('delete', () => {
+ wrapper.find('[data-test="trash"]').first().simulate('click');
+ expect(wrapper.find(ConfirmStatusChange)).toExist();
+ });
+
+ it('card view delete', () => {
+ wrapper.find('[data-test="trash"]').last().simulate('click');
+ expect(wrapper.find(ConfirmStatusChange)).toExist();
+ });
});
diff --git a/superset-frontend/src/components/ListView/CardSortSelect.tsx b/superset-frontend/src/components/ListView/CardSortSelect.tsx
new file mode 100644
index 0000000..1dbad37
--- /dev/null
+++ b/superset-frontend/src/components/ListView/CardSortSelect.tsx
@@ -0,0 +1,114 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React, { useState } from 'react';
+import { styled, withTheme, SupersetThemeProps } from '@superset-ui/style';
+import { PartialThemeConfig, Select } from 'src/components/Select';
+import { CardSortSelectOption, FetchDataConfig, SortColumn } from './types';
+import { filterSelectStyles } from './utils';
+
+const SortTitle = styled.label`
+ font-weight: bold;
+ line-height: 27px;
+ margin: 0 0.4em 0 0;
+`;
+
+const SortContainer = styled.div`
+ display: inline-flex;
+ float: right;
+ font-size: ${({ theme }) => theme.typography.sizes.s}px;
+ padding: 24px 24px 0 0;
+ position: relative;
+ top: 8px;
+`;
+interface CardViewSelectSortProps {
+ onChange: (conf: FetchDataConfig) => any;
+ options: Array<CardSortSelectOption>;
+ initialSort?: SortColumn[];
+ pageIndex: number;
+ pageSize: number;
+}
+
+interface StyledSelectProps {
+ onChange: (value: CardSortSelectOption) => void;
+ options: CardSortSelectOption[];
+ selectStyles: any;
+ theme: SupersetThemeProps['theme'];
+ value: CardSortSelectOption;
+}
+
+function StyledSelect({
+ onChange,
+ options,
+ selectStyles,
+ theme,
+ value,
+}: StyledSelectProps) {
+ const filterSelectTheme: PartialThemeConfig = {
+ spacing: {
+ baseUnit: 1,
+ fontSize: theme.typography.sizes.s,
+ minWidth: '5em',
+ },
+ };
+ return (
+ <Select
+ data-test="card-sort-select"
+ clearable={false}
+ onChange={onChange}
+ options={options}
+ stylesConfig={selectStyles}
+ themeConfig={filterSelectTheme}
+ value={value}
+ />
+ );
+}
+
+const StyledCardSortSelect = withTheme(StyledSelect);
+
+export const CardSortSelect = ({
+ initialSort,
+ onChange,
+ options,
+ pageIndex,
+ pageSize,
+}: CardViewSelectSortProps) => {
+ const defaultSort =
+ initialSort && options.find(({ id }) => id === initialSort[0].id);
+ const [selectedOption, setSelectedOption] = useState<CardSortSelectOption>(
+ defaultSort || options[0],
+ );
+
+ const handleOnChange = (selected: CardSortSelectOption) => {
+ setSelectedOption(selected);
+ const sortBy = [{ id: selected.id, desc: selected.desc }];
+ onChange({ pageIndex, pageSize, sortBy, filters: [] });
+ };
+
+ return (
+ <SortContainer>
+ <SortTitle>Sort:</SortTitle>
+ <StyledCardSortSelect
+ onChange={(value: CardSortSelectOption) => handleOnChange(value)}
+ options={options}
+ selectStyles={filterSelectStyles}
+ value={selectedOption}
+ />
+ </SortContainer>
+ );
+};
diff --git a/superset-frontend/src/components/ListView/Filters.tsx b/superset-frontend/src/components/ListView/Filters.tsx
index a27a1d6..a205031 100644
--- a/superset-frontend/src/components/ListView/Filters.tsx
+++ b/superset-frontend/src/components/ListView/Filters.tsx
@@ -23,29 +23,29 @@ import {
Select,
PaginatedSelect,
PartialThemeConfig,
- PartialStylesConfig,
} from 'src/components/Select';
import SearchInput from 'src/components/SearchInput';
import {
Filter,
- Filters,
FilterValue,
+ Filters,
InternalFilter,
SelectOption,
} from './types';
+import { filterSelectStyles } from './utils';
interface BaseFilter {
Header: string;
initialValue: any;
}
interface SelectFilterProps extends BaseFilter {
- name?: string;
- onSelect: (selected: any) => any;
- selects: Filter['selects'];
emptyLabel?: string;
fetchSelects?: Filter['fetchSelects'];
+ name?: string;
+ onSelect: (selected: any) => any;
paginate?: boolean;
+ selects: Filter['selects'];
theme: SupersetThemeProps['theme'];
}
@@ -61,40 +61,23 @@ const FilterTitle = styled.label`
margin: 0 0.4em 0 0;
`;
-const filterSelectStyles: PartialStylesConfig = {
- container: (provider, { getValue }) => ({
- ...provider,
- // dynamic width based on label string length
- minWidth: `${Math.min(
- 12,
- Math.max(5, 3 + getValue()[0].label.length / 2),
- )}em`,
- }),
- control: provider => ({
- ...provider,
- borderWidth: 0,
- boxShadow: 'none',
- cursor: 'pointer',
- }),
-};
-
const CLEAR_SELECT_FILTER_VALUE = 'CLEAR_SELECT_FILTER_VALUE';
function SelectFilter({
Header,
- selects = [],
emptyLabel = 'None',
+ fetchSelects,
initialValue,
onSelect,
- fetchSelects,
paginate = false,
+ selects = [],
theme,
}: SelectFilterProps) {
const filterSelectTheme: PartialThemeConfig = {
spacing: {
baseUnit: 2,
- minWidth: '5em',
fontSize: theme.typography.sizes.s,
+ minWidth: '5em',
},
};
@@ -235,12 +218,12 @@ function UIFilters({
(
{
Header,
+ fetchSelects,
id,
input,
+ paginate,
selects,
unfilteredLabel,
- fetchSelects,
- paginate,
},
index,
) => {
@@ -249,24 +232,24 @@ function UIFilters({
if (input === 'select') {
return (
<StyledSelectFilter
- key={id}
- name={id}
Header={Header}
- selects={selects}
emptyLabel={unfilteredLabel}
- initialValue={initialValue}
fetchSelects={fetchSelects}
- paginate={paginate}
+ initialValue={initialValue}
+ key={id}
+ name={id}
onSelect={(value: any) => updateFilterValue(index, value)}
+ paginate={paginate}
+ selects={selects}
/>
);
}
if (input === 'search') {
return (
<SearchFilter
- key={id}
Header={Header}
initialValue={initialValue}
+ key={id}
onSubmit={(value: string) => updateFilterValue(index, value)}
/>
);
diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx
index 1a8313b..8a50c26 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -28,7 +28,13 @@ import TableCollection from './TableCollection';
import CardCollection from './CardCollection';
import Pagination from './Pagination';
import FilterControls from './Filters';
-import { FetchDataConfig, Filters, SortColumn } from './types';
+import { CardSortSelect } from './CardSortSelect';
+import {
+ FetchDataConfig,
+ Filters,
+ SortColumn,
+ CardSortSelectOption,
+} from './types';
import { ListViewError, useListViewState } from './utils';
const ListViewStyles = styled.div`
@@ -188,6 +194,7 @@ export interface ListViewProps<T = any> {
disableBulkSelect?: () => void;
renderBulkSelectCopy?: (selects: any[]) => React.ReactNode;
renderCard?: (row: T) => React.ReactNode;
+ cardSortSelectOptions?: Array<CardSortSelectOption>;
}
const ListView: FunctionComponent<ListViewProps> = ({
@@ -205,6 +212,7 @@ const ListView: FunctionComponent<ListViewProps> = ({
disableBulkSelect = () => {},
renderBulkSelectCopy = selected => t('%s Selected', selected.length),
renderCard,
+ cardSortSelectOptions,
}) => {
const {
getTableProps,
@@ -263,6 +271,15 @@ const ListView: FunctionComponent<ListViewProps> = ({
updateFilterValue={applyFilterValue}
/>
)}
+ {viewingMode === 'card' && cardSortSelectOptions && (
+ <CardSortSelect
+ initialSort={initialSort}
+ onChange={fetchData}
+ options={cardSortSelectOptions}
+ pageIndex={pageIndex}
+ pageSize={pageSize}
+ />
+ )}
</div>
<div className="body">
{bulkSelectEnabled && (
diff --git a/superset-frontend/src/components/ListView/types.ts b/superset-frontend/src/components/ListView/types.ts
index 8a8b4f7..1161a88 100644
--- a/superset-frontend/src/components/ListView/types.ts
+++ b/superset-frontend/src/components/ListView/types.ts
@@ -28,6 +28,13 @@ export interface SelectOption {
value: any;
}
+export interface CardSortSelectOption {
+ desc: boolean;
+ id: any;
+ label: string;
+ value: any;
+}
+
export interface Filter {
Header: string;
id: string;
diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts
index 89cd7a3..026d9e7 100644
--- a/superset-frontend/src/components/ListView/utils.ts
+++ b/superset-frontend/src/components/ListView/utils.ts
@@ -34,7 +34,7 @@ import {
} from 'use-query-params';
import { isEqual } from 'lodash';
-
+import { PartialStylesConfig } from 'src/components/Select';
import {
FetchDataConfig,
Filter,
@@ -255,3 +255,20 @@ export function useListViewState({
applyFilterValue,
};
}
+
+export const filterSelectStyles: PartialStylesConfig = {
+ container: (provider, { getValue }) => ({
+ ...provider,
+ // dynamic width based on label string length
+ minWidth: `${Math.min(
+ 12,
+ Math.max(5, 3 + getValue()[0].label.length / 2),
+ )}em`,
+ }),
+ control: provider => ({
+ ...provider,
+ borderWidth: 0,
+ boxShadow: 'none',
+ cursor: 'pointer',
+ }),
+};
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index 785a238..d1afb78 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -342,6 +342,27 @@ class ChartList extends React.PureComponent<Props, State> {
},
];
+ sortTypes = [
+ {
+ desc: false,
+ id: 'slice_name',
+ label: 'Alphabetical',
+ value: 'alphabetical',
+ },
+ {
+ desc: true,
+ id: 'changed_on_delta_humanized',
+ label: 'Recently Modified',
+ value: 'recently_modified',
+ },
+ {
+ desc: false,
+ id: 'changed_on_delta_humanized',
+ label: 'Least Recently Modified',
+ value: 'least_recently_modified',
+ },
+ ];
+
hasPerm = (perm: string) => {
if (!this.state.permissions.length) {
return false;
@@ -592,6 +613,7 @@ class ChartList extends React.PureComponent<Props, State> {
<ListView
bulkActions={bulkActions}
bulkSelectEnabled={bulkSelectEnabled}
+ cardSortSelectOptions={this.sortTypes}
className="chart-list-view"
columns={this.columns}
count={chartCount}
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index dc4194e..bdd4936 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -266,6 +266,27 @@ class DashboardList extends React.PureComponent<Props, State> {
},
];
+ sortTypes = [
+ {
+ desc: false,
+ id: 'dashboard_title',
+ label: 'Alphabetical',
+ value: 'alphabetical',
+ },
+ {
+ desc: true,
+ id: 'changed_on_delta_humanized',
+ label: 'Recently Modified',
+ value: 'recently_modified',
+ },
+ {
+ desc: false,
+ id: 'changed_on_delta_humanized',
+ label: 'Least Recently Modified',
+ value: 'least_recently_modified',
+ },
+ ];
+
hasPerm = (perm: string) => {
if (!this.state.permissions.length) {
return false;
@@ -601,6 +622,7 @@ class DashboardList extends React.PureComponent<Props, State> {
<ListView
bulkActions={bulkActions}
bulkSelectEnabled={bulkSelectEnabled}
+ cardSortSelectOptions={this.sortTypes}
className="dashboard-list-view"
columns={this.columns}
count={dashboardCount}