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 6ff96cf refactor: useListViewResource hook for charts, dashboards,
datasets (#10680)
6ff96cf is described below
commit 6ff96cfc7202d5ef5ad31ec76bff26daef6bd8fc
Author: ʈᵃᵢ <[email protected]>
AuthorDate: Wed Aug 26 15:39:18 2020 -0700
refactor: useListViewResource hook for charts, dashboards, datasets (#10680)
---
.../views/CRUD/chart/ChartList_spec.jsx | 12 +-
.../views/CRUD/dashboard/DashboardList_spec.jsx | 11 +-
.../src/views/CRUD/chart/ChartList.tsx | 704 ++++++++----------
.../src/views/CRUD/dashboard/DashboardList.tsx | 783 +++++++++------------
.../src/views/CRUD/data/dataset/DatasetList.tsx | 735 +++++++++----------
superset-frontend/src/views/CRUD/hooks.ts | 224 ++++++
superset-frontend/src/views/CRUD/types.ts | 22 +
superset-frontend/src/views/CRUD/utils.tsx | 56 +-
8 files changed, 1216 insertions(+), 1331 deletions(-)
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 d640116..73dd9ee 100644
--- a/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
+++ b/superset-frontend/spec/javascripts/views/CRUD/chart/ChartList_spec.jsx
@@ -17,18 +17,18 @@
* under the License.
*/
import React from 'react';
-import { mount } from 'enzyme';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
-import { supersetTheme, ThemeProvider } from '@superset-ui/style';
+
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import { styledMount as mount } from 'spec/helpers/theming';
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';
-
// store needed for withToasts(ChartTable)
const mockStore = configureStore([thunk]);
const store = mockStore({});
@@ -78,8 +78,10 @@ describe('ChartList', () => {
const mockedProps = {};
const wrapper = mount(<ChartList {...mockedProps} />, {
context: { store },
- wrappingComponent: ThemeProvider,
- wrappingComponentProps: { theme: supersetTheme },
+ });
+
+ beforeAll(async () => {
+ await waitForComponentToPaint(wrapper);
});
it('renders', () => {
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 7eb634c..fd82414 100644
---
a/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
+++
b/superset-frontend/spec/javascripts/views/CRUD/dashboard/DashboardList_spec.jsx
@@ -17,11 +17,12 @@
* under the License.
*/
import React from 'react';
-import { mount } from 'enzyme';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
-import { supersetTheme, ThemeProvider } from '@superset-ui/style';
+
+import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
+import { styledMount as mount } from 'spec/helpers/theming';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import DashboardList from 'src/views/CRUD/dashboard/DashboardList';
@@ -69,8 +70,10 @@ describe('DashboardList', () => {
const mockedProps = {};
const wrapper = mount(<DashboardList {...mockedProps} />, {
context: { store },
- wrappingComponent: ThemeProvider,
- wrappingComponentProps: { theme: supersetTheme },
+ });
+
+ beforeAll(async () => {
+ await waitForComponentToPaint(wrapper);
});
it('renders', () => {
diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
index fb8d905..514a93a 100644
--- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx
+++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx
@@ -19,15 +19,11 @@
import { SupersetClient } from '@superset-ui/connection';
import { t } from '@superset-ui/translation';
import { getChartMetadataRegistry } from '@superset-ui/chart';
-import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useState, useMemo } from 'react';
import rison from 'rison';
import { uniqBy } from 'lodash';
-import {
- createFetchRelated,
- createErrorHandler,
- createFaveStarHandlers,
-} from 'src/views/CRUD/utils';
+import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
+import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import SubMenu from 'src/components/Menu/SubMenu';
import AvatarIcon from 'src/components/AvatarIcon';
@@ -35,7 +31,6 @@ import Icon from 'src/components/Icon';
import FaveStar from 'src/components/FaveStar';
import ListView, {
ListViewProps,
- FetchDataConfig,
Filters,
SelectOption,
} from 'src/components/ListView';
@@ -49,23 +44,6 @@ import { Dropdown, Menu } from 'src/common/components';
const PAGE_SIZE = 25;
const FAVESTAR_BASE_URL = '/superset/favstar/slice';
-interface Props {
- addDangerToast: (msg: string) => void;
- addSuccessToast: (msg: string) => void;
-}
-
-interface State {
- bulkSelectEnabled: boolean;
- chartCount: number;
- charts: Chart[];
- favoriteStatus: object;
- lastFetchDataConfig: FetchDataConfig | null;
- loading: boolean;
- permissions: string[];
- // for now we need to use the Slice type defined in PropertiesModal.
- // In future it would be better to have a unified Chart entity.
- sliceCurrentlyEditing: Slice | null;
-}
const createFetchDatasets = (handleError: (err: Response) => void) => async (
filterValue = '',
pageIndex?: number,
@@ -102,190 +80,239 @@ const createFetchDatasets = (handleError: (err:
Response) => void) => async (
}
return [];
};
-class ChartList extends React.PureComponent<Props, State> {
- static propTypes = {
- addDangerToast: PropTypes.func.isRequired,
- };
- state: State = {
- bulkSelectEnabled: false,
- chartCount: 0,
- charts: [],
- favoriteStatus: {}, // Hash mapping dashboard id to 'isStarred' status
- lastFetchDataConfig: null,
- loading: true,
- permissions: [],
- sliceCurrentlyEditing: null,
- };
+interface ChartListProps {
+ addDangerToast: (msg: string) => void;
+ addSuccessToast: (msg: string) => void;
+}
+
+function ChartList(props: ChartListProps) {
+ const {
+ state: {
+ loading,
+ resourceCount: chartCount,
+ resourceCollection: charts,
+ bulkSelectEnabled,
+ },
+ setResourceCollection: setCharts,
+ hasPerm,
+ fetchData,
+ toggleBulkSelect,
+ refreshData,
+ } = useListViewResource<Chart>('chart', t('chart'), props.addDangerToast);
+ const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
+ {},
+ FAVESTAR_BASE_URL,
+ props.addDangerToast,
+ );
+ const [
+ sliceCurrentlyEditing,
+ setSliceCurrentlyEditing,
+ ] = useState<Slice | null>(null);
+
+ const canEdit = hasPerm('can_edit');
+ const canDelete = hasPerm('can_delete');
+ const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
+
+ function openChartEditModal(chart: Chart) {
+ setSliceCurrentlyEditing({
+ slice_id: chart.id,
+ slice_name: chart.slice_name,
+ description: chart.description,
+ cache_timeout: chart.cache_timeout,
+ });
+ }
+
+ function closeChartEditModal() {
+ setSliceCurrentlyEditing(null);
+ }
+
+ function handleChartUpdated(edits: Chart) {
+ // update the chart in our state with the edited info
+ const newCharts = charts.map(chart =>
+ chart.id === edits.id ? { ...chart, ...edits } : chart,
+ );
+ setCharts(newCharts);
+ }
+
+ function handleChartDelete({ id, slice_name: sliceName }: Chart) {
+ SupersetClient.delete({
+ endpoint: `/api/v1/chart/${id}`,
+ }).then(
+ () => {
+ refreshData();
+ props.addSuccessToast(t('Deleted: %s', sliceName));
+ },
+ () => {
+ props.addDangerToast(t('There was an issue deleting: %s', sliceName));
+ },
+ );
+ }
- componentDidMount() {
- SupersetClient.get({
- endpoint: `/api/v1/chart/_info`,
+ function handleBulkChartDelete(chartsToDelete: Chart[]) {
+ SupersetClient.delete({
+ endpoint: `/api/v1/chart/?q=${rison.encode(
+ chartsToDelete.map(({ id }) => id),
+ )}`,
}).then(
- ({ json: infoJson = {} }) => {
- this.setState({
- permissions: infoJson.permissions,
- });
+ ({ json = {} }) => {
+ refreshData();
+ props.addSuccessToast(json.message);
},
createErrorHandler(errMsg =>
- this.props.addDangerToast(
- t('An error occurred while fetching chart info: %s', errMsg),
+ props.addDangerToast(
+ t('There was an issue deleting the selected charts: %s', errMsg),
),
),
);
}
- get canEdit() {
- return this.hasPerm('can_edit');
- }
-
- get canDelete() {
- return this.hasPerm('can_delete');
+ function renderFaveStar(id: number) {
+ return (
+ <FaveStar
+ itemId={id}
+ fetchFaveStar={fetchFaveStar}
+ saveFaveStar={saveFaveStar}
+ isStarred={!!favoriteStatusRef.current[id]}
+ height={20}
+ width={20}
+ />
+ );
}
- initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
-
- fetchMethods = createFaveStarHandlers(
- FAVESTAR_BASE_URL,
- this,
- (message: string) => {
- this.props.addDangerToast(message);
- },
- );
-
- columns = [
- {
- Cell: ({ row: { original } }: any) => {
- return (
- <FaveStar
- itemId={original.id}
- fetchFaveStar={this.fetchMethods.fetchFaveStar}
- saveFaveStar={this.fetchMethods.saveFaveStar}
- isStarred={!!this.state.favoriteStatus[original.id]}
- height={20}
- />
- );
+ const columns = useMemo(
+ () => [
+ {
+ Cell: ({
+ row: {
+ original: { id },
+ },
+ }: any) => renderFaveStar(id),
+ Header: '',
+ id: 'favorite',
+ disableSortBy: true,
},
- Header: '',
- id: 'favorite',
- disableSortBy: true,
- },
- {
- Cell: ({
- row: {
- original: { url, slice_name: sliceName },
- },
- }: any) => <a href={url}>{sliceName}</a>,
- Header: t('Chart'),
- accessor: 'slice_name',
- },
- {
- Cell: ({
- row: {
- original: { viz_type: vizType },
- },
- }: any) => vizType,
- Header: t('Visualization Type'),
- accessor: 'viz_type',
- },
- {
- Cell: ({
- row: {
- original: { datasource_name_text: dsNameTxt, datasource_url: dsUrl },
- },
- }: any) => <a href={dsUrl}>{dsNameTxt}</a>,
- Header: t('Datasource'),
- accessor: 'datasource_name',
- },
- {
- Cell: ({
- row: {
- original: {
- changed_by_name: changedByName,
- changed_by_url: changedByUrl,
+ {
+ Cell: ({
+ row: {
+ original: { url, slice_name: sliceName },
},
- },
- }: any) => <a href={changedByUrl}>{changedByName}</a>,
- Header: t('Modified By'),
- accessor: 'changed_by.first_name',
- },
- {
- Cell: ({
- row: {
- original: { changed_on_delta_humanized: changedOn },
- },
- }: any) => <span className="no-wrap">{changedOn}</span>,
- Header: t('Last Modified'),
- accessor: 'changed_on_delta_humanized',
- },
- {
- accessor: 'description',
- hidden: true,
- disableSortBy: true,
- },
- {
- accessor: 'owners',
- hidden: true,
- disableSortBy: true,
- },
- {
- accessor: 'datasource_id',
- hidden: true,
- disableSortBy: true,
- },
- {
- Cell: ({ row: { original } }: any) => {
- const handleDelete = () => this.handleChartDelete(original);
- const openEditModal = () => this.openChartEditModal(original);
- if (!this.canEdit && !this.canDelete) {
- return null;
- }
+ }: any) => <a href={url}>{sliceName}</a>,
+ Header: t('Chart'),
+ accessor: 'slice_name',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { viz_type: vizType },
+ },
+ }: any) => vizType,
+ Header: t('Visualization Type'),
+ accessor: 'viz_type',
+ },
+ {
+ Cell: ({
+ row: {
+ original: {
+ datasource_name_text: dsNameTxt,
+ datasource_url: dsUrl,
+ },
+ },
+ }: any) => <a href={dsUrl}>{dsNameTxt}</a>,
+ Header: t('Datasource'),
+ accessor: 'datasource_name',
+ },
+ {
+ Cell: ({
+ row: {
+ original: {
+ changed_by_name: changedByName,
+ changed_by_url: changedByUrl,
+ },
+ },
+ }: any) => <a href={changedByUrl}>{changedByName}</a>,
+ Header: t('Modified By'),
+ accessor: 'changed_by.first_name',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { changed_on_delta_humanized: changedOn },
+ },
+ }: any) => <span className="no-wrap">{changedOn}</span>,
+ Header: t('Last Modified'),
+ accessor: 'changed_on_delta_humanized',
+ },
+ {
+ accessor: 'description',
+ hidden: true,
+ disableSortBy: true,
+ },
+ {
+ accessor: 'owners',
+ hidden: true,
+ disableSortBy: true,
+ },
+ {
+ accessor: 'datasource_id',
+ hidden: true,
+ disableSortBy: true,
+ },
+ {
+ Cell: ({ row: { original } }: any) => {
+ const handleDelete = () => handleChartDelete(original);
+ const openEditModal = () => openChartEditModal(original);
+ if (!canEdit && !canDelete) {
+ return null;
+ }
- return (
- <span className="actions">
- {this.canDelete && (
- <ConfirmStatusChange
- title={t('Please Confirm')}
- description={
- <>
- {t('Are you sure you want to delete')}{' '}
- <b>{original.slice_name}</b>?
- </>
- }
- onConfirm={handleDelete}
- >
- {confirmDelete => (
- <span
- role="button"
- tabIndex={0}
- className="action-button"
- onClick={confirmDelete}
- >
- <Icon name="trash" />
- </span>
- )}
- </ConfirmStatusChange>
- )}
- {this.canEdit && (
- <span
- role="button"
- tabIndex={0}
- className="action-button"
- onClick={openEditModal}
- >
- <Icon name="pencil" />
- </span>
- )}
- </span>
- );
+ return (
+ <span className="actions">
+ {canDelete && (
+ <ConfirmStatusChange
+ title={t('Please Confirm')}
+ description={
+ <>
+ {t('Are you sure you want to delete')}{' '}
+ <b>{original.slice_name}</b>?
+ </>
+ }
+ onConfirm={handleDelete}
+ >
+ {confirmDelete => (
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={confirmDelete}
+ >
+ <Icon name="trash" />
+ </span>
+ )}
+ </ConfirmStatusChange>
+ )}
+ {canEdit && (
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={openEditModal}
+ >
+ <Icon name="pencil" />
+ </span>
+ )}
+ </span>
+ );
+ },
+ Header: t('Actions'),
+ id: 'actions',
+ disableSortBy: true,
},
- Header: t('Actions'),
- id: 'actions',
- disableSortBy: true,
- },
- ];
+ ],
+ [canEdit, canDelete, favoriteStatusRef],
+ );
- filters: Filters = [
+ const filters: Filters = [
{
Header: t('Owner'),
id: 'owners',
@@ -296,7 +323,7 @@ class ChartList extends React.PureComponent<Props, State> {
'chart',
'owners',
createErrorHandler(errMsg =>
- this.props.addDangerToast(
+ props.addDangerToast(
t(
'An error occurred while fetching chart dataset values: %s',
errMsg,
@@ -324,7 +351,7 @@ class ChartList extends React.PureComponent<Props, State> {
unfilteredLabel: 'All',
fetchSelects: createFetchDatasets(
createErrorHandler(errMsg =>
- this.props.addDangerToast(
+ props.addDangerToast(
t(
'An error occurred while fetching chart dataset values: %s',
errMsg,
@@ -342,7 +369,7 @@ class ChartList extends React.PureComponent<Props, State> {
},
];
- sortTypes = [
+ const sortTypes = [
{
desc: false,
id: 'slice_name',
@@ -363,139 +390,20 @@ class ChartList extends React.PureComponent<Props,
State> {
},
];
- hasPerm = (perm: string) => {
- if (!this.state.permissions.length) {
- return false;
- }
-
- return this.state.permissions.some(p => p === perm);
- };
-
- toggleBulkSelect = () => {
- this.setState({ bulkSelectEnabled: !this.state.bulkSelectEnabled });
- };
-
- openChartEditModal = (chart: Chart) => {
- this.setState({
- sliceCurrentlyEditing: {
- slice_id: chart.id,
- slice_name: chart.slice_name,
- description: chart.description,
- cache_timeout: chart.cache_timeout,
- },
- });
- };
-
- closeChartEditModal = () => {
- this.setState({ sliceCurrentlyEditing: null });
- };
-
- handleChartUpdated = (edits: Chart) => {
- // update the chart in our state with the edited info
- const newCharts = this.state.charts.map(chart =>
- chart.id === edits.id ? { ...chart, ...edits } : chart,
- );
- this.setState({
- charts: newCharts,
- });
- };
-
- handleChartDelete = ({ id, slice_name: sliceName }: Chart) => {
- SupersetClient.delete({
- endpoint: `/api/v1/chart/${id}`,
- }).then(
- () => {
- const { lastFetchDataConfig } = this.state;
- if (lastFetchDataConfig) {
- this.fetchData(lastFetchDataConfig);
- }
- this.props.addSuccessToast(t('Deleted: %s', sliceName));
- },
- () => {
- this.props.addDangerToast(
- t('There was an issue deleting: %s', sliceName),
- );
- },
- );
- };
-
- handleBulkChartDelete = (charts: Chart[]) => {
- SupersetClient.delete({
- endpoint: `/api/v1/chart/?q=${rison.encode(charts.map(({ id }) => id))}`,
- }).then(
- ({ json = {} }) => {
- const { lastFetchDataConfig } = this.state;
- if (lastFetchDataConfig) {
- this.fetchData(lastFetchDataConfig);
- }
- this.props.addSuccessToast(json.message);
- },
- createErrorHandler(errMsg =>
- this.props.addDangerToast(
- t('There was an issue deleting the selected charts: %s', errMsg),
- ),
- ),
- );
- };
-
- fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
- // set loading state, cache the last config for fetching data in this
component.
- this.setState({
- lastFetchDataConfig: {
- filters,
- pageIndex,
- pageSize,
- sortBy,
- },
- loading: true,
- });
-
- const filterExps = filters.map(({ id: col, operator: opr, value }) => ({
- col,
- opr,
- value,
- }));
-
- const queryParams = rison.encode({
- order_column: sortBy[0].id,
- order_direction: sortBy[0].desc ? 'desc' : 'asc',
- page: pageIndex,
- page_size: pageSize,
- ...(filterExps.length ? { filters: filterExps } : {}),
- });
-
- return SupersetClient.get({
- endpoint: `/api/v1/chart/?q=${queryParams}`,
- })
- .then(
- ({ json = {} }) => {
- this.setState({ charts: json.result, chartCount: json.count });
- },
- createErrorHandler(errMsg =>
- this.props.addDangerToast(
- t('An error occurred while fetching charts: %s', errMsg),
- ),
- ),
- )
- .finally(() => {
- this.setState({ loading: false });
- });
- };
-
- renderCard = (props: Chart & { loading: boolean }) => {
+ function renderCard(chart: Chart & { loading: boolean }) {
const menu = (
<Menu>
- {this.canDelete && (
+ {canDelete && (
<Menu.Item>
<ConfirmStatusChange
title={t('Please Confirm')}
description={
<>
{t('Are you sure you want to delete')}{' '}
- <b>{props.slice_name}</b>?
+ <b>{chart.slice_name}</b>?
</>
}
- onConfirm={() => this.handleChartDelete(props)}
+ onConfirm={() => handleChartDelete(chart)}
>
{confirmDelete => (
<div
@@ -510,11 +418,11 @@ class ChartList extends React.PureComponent<Props, State>
{
</ConfirmStatusChange>
</Menu.Item>
)}
- {this.canEdit && (
+ {canEdit && (
<Menu.Item
role="button"
tabIndex={0}
- onClick={() => this.openChartEditModal(props)}
+ onClick={() => openChartEditModal(chart)}
>
<ListViewCard.MenuIcon name="pencil" /> Edit
</Menu.Item>
@@ -524,16 +432,16 @@ class ChartList extends React.PureComponent<Props, State>
{
return (
<ListViewCard
- loading={props.loading}
- title={props.slice_name}
- url={this.state.bulkSelectEnabled ? undefined : props.url}
- imgURL={props.thumbnail_url ?? ''}
+ loading={chart.loading}
+ title={chart.slice_name}
+ url={bulkSelectEnabled ? undefined : chart.url}
+ imgURL={chart.thumbnail_url ?? ''}
imgFallbackURL={'/static/assets/images/chart-card-fallback.png'}
- description={t('Last modified %s', props.changed_on_delta_humanized)}
- coverLeft={(props.owners || []).slice(0, 5).map(owner => (
+ description={t('Last modified %s', chart.changed_on_delta_humanized)}
+ coverLeft={(chart.owners || []).slice(0, 5).map(owner => (
<AvatarIcon
key={owner.id}
- uniqueKey={`${owner.username}-${props.id}`}
+ uniqueKey={`${owner.username}-${chart.id}`}
firstName={owner.first_name}
lastName={owner.last_name}
iconSize={24}
@@ -541,18 +449,11 @@ class ChartList extends React.PureComponent<Props, State>
{
/>
))}
coverRight={
- <Label bsStyle="secondary">{props.datasource_name_text}</Label>
+ <Label bsStyle="secondary">{chart.datasource_name_text}</Label>
}
actions={
<ListViewCard.Actions>
- <FaveStar
- itemId={props.id}
- fetchFaveStar={this.fetchMethods.fetchFaveStar}
- saveFaveStar={this.fetchMethods.saveFaveStar}
- isStarred={!!this.state.favoriteStatus[props.id]}
- width={20}
- height={20}
- />
+ {renderFaveStar(chart.id)}
<Dropdown overlay={menu}>
<Icon name="more" />
</Dropdown>
@@ -560,79 +461,68 @@ class ChartList extends React.PureComponent<Props, State>
{
}
/>
);
- };
+ }
- render() {
- const {
- bulkSelectEnabled,
- charts,
- chartCount,
- loading,
- sliceCurrentlyEditing,
- } = this.state;
- return (
- <>
- <SubMenu
- name={t('Charts')}
- secondaryButton={
- this.canDelete
- ? {
- name: t('Bulk Select'),
- onClick: this.toggleBulkSelect,
- }
- : undefined
- }
+ return (
+ <>
+ <SubMenu
+ name={t('Charts')}
+ secondaryButton={
+ canDelete
+ ? {
+ name: t('Bulk Select'),
+ onClick: toggleBulkSelect,
+ }
+ : undefined
+ }
+ />
+ {sliceCurrentlyEditing && (
+ <PropertiesModal
+ onHide={closeChartEditModal}
+ onSave={handleChartUpdated}
+ show
+ slice={sliceCurrentlyEditing}
/>
- {sliceCurrentlyEditing && (
- <PropertiesModal
- onHide={this.closeChartEditModal}
- onSave={this.handleChartUpdated}
- show
- slice={sliceCurrentlyEditing}
- />
- )}
- <ConfirmStatusChange
- title={t('Please confirm')}
- description={t(
- 'Are you sure you want to delete the selected charts?',
- )}
- onConfirm={this.handleBulkChartDelete}
- >
- {confirmDelete => {
- const bulkActions: ListViewProps['bulkActions'] = this.canDelete
- ? [
- {
- key: 'delete',
- name: t('Delete'),
- onSelect: confirmDelete,
- type: 'danger',
- },
- ]
- : [];
-
- return (
- <ListView
- bulkActions={bulkActions}
- bulkSelectEnabled={bulkSelectEnabled}
- cardSortSelectOptions={this.sortTypes}
- className="chart-list-view"
- columns={this.columns}
- count={chartCount}
- data={charts}
- disableBulkSelect={this.toggleBulkSelect}
- fetchData={this.fetchData}
- filters={this.filters}
- initialSort={this.initialSort}
- loading={loading}
- pageSize={PAGE_SIZE}
- renderCard={this.renderCard}
- />
- );
- }}
- </ConfirmStatusChange>
- </>
- );
- }
+ )}
+ <ConfirmStatusChange
+ title={t('Please confirm')}
+ description={t('Are you sure you want to delete the selected charts?')}
+ onConfirm={handleBulkChartDelete}
+ >
+ {confirmDelete => {
+ const bulkActions: ListViewProps['bulkActions'] = canDelete
+ ? [
+ {
+ key: 'delete',
+ name: t('Delete'),
+ onSelect: confirmDelete,
+ type: 'danger',
+ },
+ ]
+ : [];
+
+ return (
+ <ListView
+ bulkActions={bulkActions}
+ bulkSelectEnabled={bulkSelectEnabled}
+ cardSortSelectOptions={sortTypes}
+ className="chart-list-view"
+ columns={columns}
+ count={chartCount}
+ data={charts}
+ disableBulkSelect={toggleBulkSelect}
+ fetchData={fetchData}
+ filters={filters}
+ initialSort={initialSort}
+ loading={loading}
+ pageSize={PAGE_SIZE}
+ renderCard={renderCard}
+ />
+ );
+ }}
+ </ConfirmStatusChange>
+ </>
+ );
}
export default withToasts(ChartList);
diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
index 437edbb..fded93a 100644
--- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
+++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx
@@ -18,22 +18,14 @@
*/
import { SupersetClient } from '@superset-ui/connection';
import { t } from '@superset-ui/translation';
-import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useState, useMemo } from 'react';
import rison from 'rison';
-import {
- createFetchRelated,
- createErrorHandler,
- createFaveStarHandlers,
-} from 'src/views/CRUD/utils';
+import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
+import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import SubMenu from 'src/components/Menu/SubMenu';
import AvatarIcon from 'src/components/AvatarIcon';
-import ListView, {
- ListViewProps,
- FetchDataConfig,
- Filters,
-} from 'src/components/ListView';
+import ListView, { ListViewProps, Filters } from 'src/components/ListView';
import ExpandableList from 'src/components/ExpandableList';
import Owner from 'src/types/Owner';
import withToasts from 'src/messageToasts/enhancers/withToasts';
@@ -47,22 +39,11 @@ import { Dropdown, Menu } from 'src/common/components';
const PAGE_SIZE = 25;
const FAVESTAR_BASE_URL = '/superset/favstar/Dashboard';
-interface Props {
+interface DashboardListProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
}
-interface State {
- bulkSelectEnabled: boolean;
- dashboardCount: number;
- dashboards: Dashboard[];
- favoriteStatus: object;
- dashboardToEdit: Dashboard | null;
- lastFetchDataConfig: FetchDataConfig | null;
- loading: boolean;
- permissions: string[];
-}
-
interface Dashboard {
changed_by_name: string;
changed_by_url: string;
@@ -76,157 +57,266 @@ interface Dashboard {
owners: Owner[];
}
-class DashboardList extends React.PureComponent<Props, State> {
- static propTypes = {
- addDangerToast: PropTypes.func.isRequired,
- };
+function DashboardList(props: DashboardListProps) {
+ const {
+ state: {
+ loading,
+ resourceCount: dashboardCount,
+ resourceCollection: dashboards,
+ bulkSelectEnabled,
+ },
+ setResourceCollection: setDashboards,
+ hasPerm,
+ fetchData,
+ toggleBulkSelect,
+ refreshData,
+ } = useListViewResource<Dashboard>(
+ 'dashboard',
+ t('dashboard'),
+ props.addDangerToast,
+ );
+ const [favoriteStatusRef, fetchFaveStar, saveFaveStar] = useFavoriteStatus(
+ {},
+ FAVESTAR_BASE_URL,
+ props.addDangerToast,
+ );
+
+ const [dashboardToEdit, setDashboardToEdit] = useState<Dashboard | null>(
+ null,
+ );
- state: State = {
- bulkSelectEnabled: false,
- dashboardCount: 0,
- dashboards: [],
- favoriteStatus: {}, // Hash mapping dashboard id to 'isStarred' status
- dashboardToEdit: null,
- lastFetchDataConfig: null,
- loading: true,
- permissions: [],
- };
+ const canEdit = hasPerm('can_edit');
+ const canDelete = hasPerm('can_delete');
+ const canExport = hasPerm('can_mulexport');
+
+ const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
+
+ function openDashboardEditModal(dashboard: Dashboard) {
+ setDashboardToEdit(dashboard);
+ }
- componentDidMount() {
- SupersetClient.get({
- endpoint: `/api/v1/dashboard/_info`,
+ function handleDashboardEdit(edits: Dashboard) {
+ return SupersetClient.get({
+ endpoint: `/api/v1/dashboard/${edits.id}`,
}).then(
- ({ json: infoJson = {} }) => {
- this.setState({
- permissions: infoJson.permissions,
- });
+ ({ json = {} }) => {
+ setDashboards(
+ dashboards.map(dashboard => {
+ if (dashboard.id === json.id) {
+ return json.result;
+ }
+ return dashboard;
+ }),
+ );
},
createErrorHandler(errMsg =>
- this.props.addDangerToast(
- t('An error occurred while fetching Dashboards: %s, %s', errMsg),
+ props.addDangerToast(
+ t('An error occurred while fetching dashboards: %s', errMsg),
),
),
);
}
- get canEdit() {
- return this.hasPerm('can_edit');
+ function handleDashboardDelete({
+ id,
+ dashboard_title: dashboardTitle,
+ }: Dashboard) {
+ return SupersetClient.delete({
+ endpoint: `/api/v1/dashboard/${id}`,
+ }).then(
+ () => {
+ refreshData();
+ props.addSuccessToast(t('Deleted: %s', dashboardTitle));
+ },
+ createErrorHandler(errMsg =>
+ props.addDangerToast(
+ t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
+ ),
+ ),
+ );
}
- get canDelete() {
- return this.hasPerm('can_delete');
+ function handleBulkDashboardDelete(dashboardsToDelete: Dashboard[]) {
+ return SupersetClient.delete({
+ endpoint: `/api/v1/dashboard/?q=${rison.encode(
+ dashboardsToDelete.map(({ id }) => id),
+ )}`,
+ }).then(
+ ({ json = {} }) => {
+ props.addSuccessToast(json.message);
+ },
+ createErrorHandler(errMsg =>
+ props.addDangerToast(
+ t('There was an issue deleting the selected dashboards: ', errMsg),
+ ),
+ ),
+ );
}
- get canExport() {
- return this.hasPerm('can_mulexport');
+ function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) {
+ return window.location.assign(
+ `/api/v1/dashboard/export/?q=${rison.encode(
+ dashboardsToExport.map(({ id }) => id),
+ )}`,
+ );
}
- initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
-
- fetchMethods = createFaveStarHandlers(
- FAVESTAR_BASE_URL,
- this,
- (message: string) => {
- this.props.addDangerToast(message);
- },
- );
+ function renderFaveStar(id: number) {
+ return (
+ <FaveStar
+ itemId={id}
+ fetchFaveStar={fetchFaveStar}
+ saveFaveStar={saveFaveStar}
+ isStarred={!!favoriteStatusRef.current[id]}
+ height={20}
+ width={20}
+ />
+ );
+ }
- columns = [
- {
- Cell: ({ row: { original } }: any) => {
- return (
- <FaveStar
- itemId={original.id}
- fetchFaveStar={this.fetchMethods.fetchFaveStar}
- saveFaveStar={this.fetchMethods.saveFaveStar}
- isStarred={!!this.state.favoriteStatus[original.id]}
- height={20}
+ const columns = useMemo(
+ () => [
+ {
+ Cell: ({
+ row: {
+ original: { id },
+ },
+ }: any) => renderFaveStar(id),
+ Header: '',
+ id: 'favorite',
+ disableSortBy: true,
+ },
+ {
+ Cell: ({
+ row: {
+ original: { url, dashboard_title: dashboardTitle },
+ },
+ }: any) => <a href={url}>{dashboardTitle}</a>,
+ Header: t('Title'),
+ accessor: 'dashboard_title',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { owners },
+ },
+ }: any) => (
+ <ExpandableList
+ items={owners.map(
+ ({ first_name: firstName, last_name: lastName }: any) =>
+ `${firstName} ${lastName}`,
+ )}
+ display={2}
/>
- );
+ ),
+ Header: t('Owners'),
+ accessor: 'owners',
+ disableSortBy: true,
},
- Header: '',
- id: 'favorite',
- disableSortBy: true,
- },
- {
- Cell: ({
- row: {
- original: { url, dashboard_title: dashboardTitle },
- },
- }: any) => <a href={url}>{dashboardTitle}</a>,
- Header: t('Title'),
- accessor: 'dashboard_title',
- },
- {
- Cell: ({
- row: {
- original: { owners },
- },
- }: any) => (
- <ExpandableList
- items={owners.map(
- ({ first_name: firstName, last_name: lastName }: any) =>
- `${firstName} ${lastName}`,
- )}
- display={2}
- />
- ),
- Header: t('Owners'),
- accessor: 'owners',
- disableSortBy: true,
- },
- {
- Cell: ({
- row: {
- original: {
- changed_by_name: changedByName,
- changed_by_url: changedByUrl,
+ {
+ Cell: ({
+ row: {
+ original: {
+ changed_by_name: changedByName,
+ changed_by_url: changedByUrl,
+ },
},
+ }: any) => <a href={changedByUrl}>{changedByName}</a>,
+ Header: t('Modified By'),
+ accessor: 'changed_by.first_name',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { published },
+ },
+ }: any) => (
+ <span className="no-wrap">
+ {published ? <Icon name="check" /> : ''}
+ </span>
+ ),
+ Header: t('Published'),
+ accessor: 'published',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { changed_on_delta_humanized: changedOn },
+ },
+ }: any) => <span className="no-wrap">{changedOn}</span>,
+ Header: t('Modified'),
+ accessor: 'changed_on_delta_humanized',
+ },
+ {
+ accessor: 'slug',
+ hidden: true,
+ disableSortBy: true,
+ },
+ {
+ Cell: ({ row: { original } }: any) => {
+ const handleDelete = () => handleDashboardDelete(original);
+ const handleEdit = () => openDashboardEditModal(original);
+ const handleExport = () => handleBulkDashboardExport([original]);
+ if (!canEdit && !canDelete && !canExport) {
+ return null;
+ }
+ return (
+ <span className="actions">
+ {canDelete && (
+ <ConfirmStatusChange
+ title={t('Please Confirm')}
+ description={
+ <>
+ {t('Are you sure you want to delete')}{' '}
+ <b>{original.dashboard_title}</b>?
+ </>
+ }
+ onConfirm={handleDelete}
+ >
+ {confirmDelete => (
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={confirmDelete}
+ >
+ <Icon name="trash" />
+ </span>
+ )}
+ </ConfirmStatusChange>
+ )}
+ {canExport && (
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={handleExport}
+ >
+ <Icon name="share" />
+ </span>
+ )}
+ {canEdit && (
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={handleEdit}
+ >
+ <Icon name="pencil" />
+ </span>
+ )}
+ </span>
+ );
},
- }: any) => <a href={changedByUrl}>{changedByName}</a>,
- Header: t('Modified By'),
- accessor: 'changed_by.first_name',
- },
- {
- Cell: ({
- row: {
- original: { published },
- },
- }: any) => (
- <span className="no-wrap">
- {published ? <Icon name="check" /> : ''}
- </span>
- ),
- Header: t('Published'),
- accessor: 'published',
- },
- {
- Cell: ({
- row: {
- original: { changed_on_delta_humanized: changedOn },
- },
- }: any) => <span className="no-wrap">{changedOn}</span>,
- Header: t('Modified'),
- accessor: 'changed_on_delta_humanized',
- },
- {
- accessor: 'slug',
- hidden: true,
- disableSortBy: true,
- },
- {
- Cell: ({ row: { original } }: any) => this.renderActions(original),
- Header: t('Actions'),
- id: 'actions',
- disableSortBy: true,
- },
- ];
-
- toggleBulkSelect = () => {
- this.setState({ bulkSelectEnabled: !this.state.bulkSelectEnabled });
- };
+ Header: t('Actions'),
+ id: 'actions',
+ disableSortBy: true,
+ },
+ ],
+ [canEdit, canDelete, canExport, favoriteStatusRef],
+ );
- filters: Filters = [
+ const filters: Filters = [
{
Header: 'Owner',
id: 'owners',
@@ -237,7 +327,7 @@ class DashboardList extends React.PureComponent<Props,
State> {
'dashboard',
'owners',
createErrorHandler(errMsg =>
- this.props.addDangerToast(
+ props.addDangerToast(
t(
'An error occurred while fetching chart owner values: %s',
errMsg,
@@ -266,7 +356,7 @@ class DashboardList extends React.PureComponent<Props,
State> {
},
];
- sortTypes = [
+ const sortTypes = [
{
desc: false,
id: 'dashboard_title',
@@ -287,210 +377,20 @@ class DashboardList extends React.PureComponent<Props,
State> {
},
];
- hasPerm = (perm: string) => {
- if (!this.state.permissions.length) {
- return false;
- }
-
- return Boolean(this.state.permissions.find(p => p === perm));
- };
-
- openDashboardEditModal = (dashboard: Dashboard) => {
- this.setState({
- dashboardToEdit: dashboard,
- });
- };
-
- handleDashboardEdit = (edits: any) => {
- this.setState({ loading: true });
- return SupersetClient.get({
- endpoint: `/api/v1/dashboard/${edits.id}`,
- }).then(
- ({ json = {} }) => {
- this.setState({
- dashboards: this.state.dashboards.map(dashboard => {
- if (dashboard.id === json.id) {
- return json.result;
- }
- return dashboard;
- }),
- loading: false,
- });
- },
- createErrorHandler(errMsg =>
- this.props.addDangerToast(
- t('An error occurred while fetching dashboards: %s', errMsg),
- ),
- ),
- );
- };
-
- handleDashboardDelete = ({
- id,
- dashboard_title: dashboardTitle,
- }: Dashboard) =>
- SupersetClient.delete({
- endpoint: `/api/v1/dashboard/${id}`,
- }).then(
- () => {
- const { lastFetchDataConfig } = this.state;
- if (lastFetchDataConfig) {
- this.fetchData(lastFetchDataConfig);
- }
- this.props.addSuccessToast(t('Deleted: %s', dashboardTitle));
- },
- createErrorHandler(errMsg =>
- this.props.addDangerToast(
- t('There was an issue deleting %s: %s', dashboardTitle, errMsg),
- ),
- ),
- );
-
- handleBulkDashboardDelete = (dashboards: Dashboard[]) => {
- SupersetClient.delete({
- endpoint: `/api/v1/dashboard/?q=${rison.encode(
- dashboards.map(({ id }) => id),
- )}`,
- }).then(
- ({ json = {} }) => {
- const { lastFetchDataConfig } = this.state;
- if (lastFetchDataConfig) {
- this.fetchData(lastFetchDataConfig);
- }
- this.props.addSuccessToast(json.message);
- },
- createErrorHandler(errMsg =>
- this.props.addDangerToast(
- t('There was an issue deleting the selected dashboards: ', errMsg),
- ),
- ),
- );
- };
-
- handleBulkDashboardExport = (dashboards: Dashboard[]) => {
- return window.location.assign(
- `/api/v1/dashboard/export/?q=${rison.encode(
- dashboards.map(({ id }) => id),
- )}`,
- );
- };
-
- fetchData = ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
- // set loading state, cache the last config for fetching data in this
component.
- this.setState({
- lastFetchDataConfig: {
- filters,
- pageIndex,
- pageSize,
- sortBy,
- },
- loading: true,
- });
- const filterExps = filters.map(({ id: col, operator: opr, value }) => ({
- col,
- opr,
- value,
- }));
-
- const queryParams = rison.encode({
- order_column: sortBy[0].id,
- order_direction: sortBy[0].desc ? 'desc' : 'asc',
- page: pageIndex,
- page_size: pageSize,
- ...(filterExps.length ? { filters: filterExps } : {}),
- });
-
- return SupersetClient.get({
- endpoint: `/api/v1/dashboard/?q=${queryParams}`,
- })
- .then(
- ({ json = {} }) => {
- this.setState({
- dashboards: json.result,
- dashboardCount: json.count,
- });
- },
- createErrorHandler(errMsg =>
- this.props.addDangerToast(
- t('An error occurred while fetching dashboards: %s', errMsg),
- ),
- ),
- )
- .finally(() => {
- this.setState({ loading: false });
- });
- };
-
- renderActions(original: Dashboard) {
- const handleDelete = () => this.handleDashboardDelete(original);
- const handleEdit = () => this.openDashboardEditModal(original);
- const handleExport = () => this.handleBulkDashboardExport([original]);
- if (!this.canEdit && !this.canDelete && !this.canExport) {
- return null;
- }
- return (
- <span className="actions">
- {this.canDelete && (
- <ConfirmStatusChange
- title={t('Please Confirm')}
- description={
- <>
- {t('Are you sure you want to delete')}{' '}
- <b>{original.dashboard_title}</b>?
- </>
- }
- onConfirm={handleDelete}
- >
- {confirmDelete => (
- <span
- role="button"
- tabIndex={0}
- className="action-button"
- onClick={confirmDelete}
- >
- <Icon name="trash" />
- </span>
- )}
- </ConfirmStatusChange>
- )}
- {this.canExport && (
- <span
- role="button"
- tabIndex={0}
- className="action-button"
- onClick={handleExport}
- >
- <Icon name="share" />
- </span>
- )}
- {this.canEdit && (
- <span
- role="button"
- tabIndex={0}
- className="action-button"
- onClick={handleEdit}
- >
- <Icon name="pencil" />
- </span>
- )}
- </span>
- );
- }
-
- renderCard = (props: Dashboard & { loading: boolean }) => {
+ function renderCard(dashboard: Dashboard & { loading: boolean }) {
const menu = (
<Menu>
- {this.canDelete && (
+ {canDelete && (
<Menu.Item>
<ConfirmStatusChange
title={t('Please Confirm')}
description={
<>
{t('Are you sure you want to delete')}{' '}
- <b>{props.dashboard_title}</b>?
+ <b>{dashboard.dashboard_title}</b>?
</>
}
- onConfirm={() => this.handleDashboardDelete(props)}
+ onConfirm={() => handleDashboardDelete(dashboard)}
>
{confirmDelete => (
<div
@@ -505,20 +405,20 @@ class DashboardList extends React.PureComponent<Props,
State> {
</ConfirmStatusChange>
</Menu.Item>
)}
- {this.canExport && (
+ {canExport && (
<Menu.Item
role="button"
tabIndex={0}
- onClick={() => this.handleBulkDashboardExport([props])}
+ onClick={() => handleBulkDashboardExport([dashboard])}
>
<ListViewCard.MenuIcon name="share" /> Export
</Menu.Item>
)}
- {this.canEdit && (
+ {canEdit && (
<Menu.Item
role="button"
tabIndex={0}
- onClick={() => this.openDashboardEditModal(props)}
+ onClick={() => openDashboardEditModal(dashboard)}
>
<ListViewCard.MenuIcon name="pencil" /> Edit
</Menu.Item>
@@ -528,17 +428,22 @@ class DashboardList extends React.PureComponent<Props,
State> {
return (
<ListViewCard
- title={props.dashboard_title}
- loading={props.loading}
- titleRight={<Label>{props.published ? 'published' : 'draft'}</Label>}
- url={this.state.bulkSelectEnabled ? undefined : props.url}
- imgURL={props.thumbnail_url}
+ loading={dashboard.loading}
+ title={dashboard.dashboard_title}
+ titleRight={
+ <Label>{dashboard.published ? 'published' : 'draft'}</Label>
+ }
+ url={bulkSelectEnabled ? undefined : dashboard.url}
+ imgURL={dashboard.thumbnail_url}
imgFallbackURL="/static/assets/images/dashboard-card-fallback.png"
- description={t('Last modified %s', props.changed_on_delta_humanized)}
- coverLeft={(props.owners || []).slice(0, 5).map(owner => (
+ description={t(
+ 'Last modified %s',
+ dashboard.changed_on_delta_humanized,
+ )}
+ coverLeft={(dashboard.owners || []).slice(0, 5).map(owner => (
<AvatarIcon
key={owner.id}
- uniqueKey={`${owner.username}-${props.id}`}
+ uniqueKey={`${owner.username}-${dashboard.id}`}
firstName={owner.first_name}
lastName={owner.last_name}
iconSize={24}
@@ -547,14 +452,7 @@ class DashboardList extends React.PureComponent<Props,
State> {
))}
actions={
<ListViewCard.Actions>
- <FaveStar
- itemId={props.id}
- fetchFaveStar={this.fetchMethods.fetchFaveStar}
- saveFaveStar={this.fetchMethods.saveFaveStar}
- isStarred={!!this.state.favoriteStatus[props.id]}
- width={20}
- height={20}
- />
+ {renderFaveStar(dashboard.id)}
<Dropdown overlay={menu}>
<Icon name="more" />
</Dropdown>
@@ -562,87 +460,78 @@ class DashboardList extends React.PureComponent<Props,
State> {
}
/>
);
- };
+ }
- render() {
- const {
- bulkSelectEnabled,
- dashboards,
- dashboardCount,
- loading,
- dashboardToEdit,
- } = this.state;
- return (
- <>
- <SubMenu
- name={t('Dashboards')}
- secondaryButton={
- this.canDelete || this.canExport
- ? {
- name: t('Bulk Select'),
- onClick: this.toggleBulkSelect,
- }
- : undefined
+ return (
+ <>
+ <SubMenu
+ name={t('Dashboards')}
+ secondaryButton={
+ canDelete || canExport
+ ? {
+ name: t('Bulk Select'),
+ onClick: toggleBulkSelect,
+ }
+ : undefined
+ }
+ />
+ <ConfirmStatusChange
+ title={t('Please confirm')}
+ description={t(
+ 'Are you sure you want to delete the selected dashboards?',
+ )}
+ onConfirm={handleBulkDashboardDelete}
+ >
+ {confirmDelete => {
+ const bulkActions: ListViewProps['bulkActions'] = [];
+ if (canDelete) {
+ bulkActions.push({
+ key: 'delete',
+ name: t('Delete'),
+ type: 'danger',
+ onSelect: confirmDelete,
+ });
}
- />
- <ConfirmStatusChange
- title={t('Please confirm')}
- description={t(
- 'Are you sure you want to delete the selected dashboards?',
- )}
- onConfirm={this.handleBulkDashboardDelete}
- >
- {confirmDelete => {
- const bulkActions: ListViewProps['bulkActions'] = [];
- if (this.canDelete) {
- bulkActions.push({
- key: 'delete',
- name: t('Delete'),
- type: 'danger',
- onSelect: confirmDelete,
- });
- }
- if (this.canExport) {
- bulkActions.push({
- key: 'export',
- name: t('Export'),
- type: 'primary',
- onSelect: this.handleBulkDashboardExport,
- });
- }
- return (
- <>
- {dashboardToEdit && (
- <PropertiesModal
- dashboardId={dashboardToEdit.id}
- show
- onHide={() => this.setState({ dashboardToEdit: null })}
- onSubmit={this.handleDashboardEdit}
- />
- )}
- <ListView
- bulkActions={bulkActions}
- bulkSelectEnabled={bulkSelectEnabled}
- cardSortSelectOptions={this.sortTypes}
- className="dashboard-list-view"
- columns={this.columns}
- count={dashboardCount}
- data={dashboards}
- disableBulkSelect={this.toggleBulkSelect}
- fetchData={this.fetchData}
- filters={this.filters}
- initialSort={this.initialSort}
- loading={loading}
- pageSize={PAGE_SIZE}
- renderCard={this.renderCard}
+ if (canExport) {
+ bulkActions.push({
+ key: 'export',
+ name: t('Export'),
+ type: 'primary',
+ onSelect: handleBulkDashboardExport,
+ });
+ }
+ return (
+ <>
+ {dashboardToEdit && (
+ <PropertiesModal
+ dashboardId={dashboardToEdit.id}
+ show
+ onHide={() => setDashboardToEdit(null)}
+ onSubmit={handleDashboardEdit}
/>
- </>
- );
- }}
- </ConfirmStatusChange>
- </>
- );
- }
+ )}
+ <ListView
+ bulkActions={bulkActions}
+ bulkSelectEnabled={bulkSelectEnabled}
+ cardSortSelectOptions={sortTypes}
+ className="dashboard-list-view"
+ columns={columns}
+ count={dashboardCount}
+ data={dashboards}
+ disableBulkSelect={toggleBulkSelect}
+ fetchData={fetchData}
+ filters={filters}
+ initialSort={initialSort}
+ loading={loading}
+ pageSize={PAGE_SIZE}
+ renderCard={renderCard}
+ />
+ </>
+ );
+ }}
+ </ConfirmStatusChange>
+ </>
+ );
}
export default withToasts(DashboardList);
diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
index f7a5ab0..668739c 100644
--- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
+++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx
@@ -18,22 +18,14 @@
*/
import { SupersetClient } from '@superset-ui/connection';
import { t } from '@superset-ui/translation';
-import React, {
- FunctionComponent,
- useCallback,
- useEffect,
- useState,
-} from 'react';
+import React, { FunctionComponent, useState, useMemo } from 'react';
import rison from 'rison';
import { createFetchRelated, createErrorHandler } from 'src/views/CRUD/utils';
+import { useListViewResource } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
import DatasourceModal from 'src/datasource/DatasourceModal';
import DeleteModal from 'src/components/DeleteModal';
-import ListView, {
- ListViewProps,
- FetchDataConfig,
- Filters,
-} from 'src/components/ListView';
+import ListView, { ListViewProps, Filters } from 'src/components/ListView';
import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu';
import { commonMenuData } from 'src/views/CRUD/data/common';
import AvatarIcon from 'src/components/AvatarIcon';
@@ -54,6 +46,7 @@ type Dataset = {
id: string;
database_name: string;
};
+ kind: string;
explore_url: string;
id: number;
owners: Array<Owner>;
@@ -104,118 +97,31 @@ const DatasetList: FunctionComponent<DatasetListProps> =
({
addDangerToast,
addSuccessToast,
}) => {
- const [datasetCount, setDatasetCount] = useState(0);
+ const {
+ state: {
+ loading,
+ resourceCount: datasetCount,
+ resourceCollection: datasets,
+ bulkSelectEnabled,
+ },
+ hasPerm,
+ fetchData,
+ toggleBulkSelect,
+ refreshData,
+ } = useListViewResource<Dataset>('dataset', t('dataset'), addDangerToast);
+
+ const [datasetAddModalOpen, setDatasetAddModalOpen] = useState<boolean>(
+ false,
+ );
+
const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
(Dataset & { chart_count: number; dashboard_count: number }) | null
>(null);
+
const [
datasetCurrentlyEditing,
setDatasetCurrentlyEditing,
] = useState<Dataset | null>(null);
- const [datasets, setDatasets] = useState<any[]>([]);
- const [
- lastFetchDataConfig,
- setLastFetchDataConfig,
- ] = useState<FetchDataConfig | null>(null);
- const [loading, setLoading] = useState(true);
- const [permissions, setPermissions] = useState<string[]>([]);
-
- const [datasetAddModalOpen, setDatasetAddModalOpen] = useState<boolean>(
- false,
- );
- const [bulkSelectEnabled, setBulkSelectEnabled] = useState<boolean>(false);
-
- const filterTypes: Filters = [
- {
- Header: t('Owner'),
- id: 'owners',
- input: 'select',
- operator: 'rel_m_m',
- unfilteredLabel: 'All',
- fetchSelects: createFetchRelated(
- 'dataset',
- 'owners',
- createErrorHandler(errMsg =>
- t(
- 'An error occurred while fetching dataset owner values: %s',
- errMsg,
- ),
- ),
- ),
- paginate: true,
- },
- {
- Header: t('Datasource'),
- id: 'database',
- input: 'select',
- operator: 'rel_o_m',
- unfilteredLabel: 'All',
- fetchSelects: createFetchRelated(
- 'dataset',
- 'database',
- createErrorHandler(errMsg =>
- t(
- 'An error occurred while fetching dataset datasource values: %s',
- errMsg,
- ),
- ),
- ),
- paginate: true,
- },
- {
- Header: t('Schema'),
- id: 'schema',
- input: 'select',
- operator: 'eq',
- unfilteredLabel: 'All',
- fetchSelects: createFetchSchemas(errMsg =>
- t('An error occurred while fetching schema values: %s', errMsg),
- ),
- paginate: true,
- },
- {
- Header: t('Type'),
- id: 'is_sqllab_view',
- input: 'select',
- operator: 'eq',
- unfilteredLabel: 'All',
- selects: [
- { label: 'Virtual', value: true },
- { label: 'Physical', value: false },
- ],
- },
- {
- Header: t('Search'),
- id: 'table_name',
- input: 'search',
- operator: 'ct',
- },
- ];
-
- const fetchDatasetInfo = () => {
- SupersetClient.get({
- endpoint: `/api/v1/dataset/_info`,
- }).then(
- ({ json: infoJson = {} }) => {
- setPermissions(infoJson.permissions);
- },
- createErrorHandler(errMsg =>
- addDangerToast(t('An error occurred while fetching datasets', errMsg)),
- ),
- );
- };
-
- useEffect(() => {
- fetchDatasetInfo();
- }, []);
-
- const hasPerm = (perm: string) => {
- if (!permissions.length) {
- return false;
- }
-
- return Boolean(permissions.find(p => p === perm));
- };
const canEdit = hasPerm('can_edit');
const canDelete = hasPerm('can_delete');
@@ -258,187 +164,260 @@ const DatasetList: FunctionComponent<DatasetListProps>
= ({
),
);
- const columns = [
- {
- Cell: ({
- row: {
- original: { kind },
- },
- }: any) => {
- if (kind === 'physical')
+ const columns = useMemo(
+ () => [
+ {
+ Cell: ({
+ row: {
+ original: { kind },
+ },
+ }: any) => {
+ if (kind === 'physical')
+ return (
+ <TooltipWrapper
+ label="physical-dataset"
+ tooltip={t('Physical Dataset')}
+ >
+ <Icon name="dataset-physical" />
+ </TooltipWrapper>
+ );
+
return (
<TooltipWrapper
- label="physical-dataset"
- tooltip={t('Physical Dataset')}
+ label="virtual-dataset"
+ tooltip={t('Virtual Dataset')}
>
- <Icon name="dataset-physical" />
+ <Icon name="dataset-virtual" />
</TooltipWrapper>
);
-
- return (
- <TooltipWrapper
- label="virtual-dataset"
- tooltip={t('Virtual Dataset')}
- >
- <Icon name="dataset-virtual" />
- </TooltipWrapper>
- );
- },
- accessor: 'kind_icon',
- disableSortBy: true,
- size: 'xs',
- },
- {
- Cell: ({
- row: {
- original: { table_name: datasetTitle },
- },
- }: any) => datasetTitle,
- Header: t('Name'),
- accessor: 'table_name',
- },
- {
- Cell: ({
- row: {
- original: { kind },
},
- }: any) => kind[0]?.toUpperCase() + kind.slice(1),
- Header: t('Type'),
- accessor: 'kind',
- disableSortBy: true,
- size: 'md',
- },
- {
- Header: t('Source'),
- accessor: 'database.database_name',
- size: 'lg',
- },
- {
- Header: t('Schema'),
- accessor: 'schema',
- size: 'lg',
- },
- {
- Cell: ({
- row: {
- original: { changed_on_delta_humanized: changedOn },
- },
- }: any) => <span className="no-wrap">{changedOn}</span>,
- Header: t('Modified'),
- accessor: 'changed_on_delta_humanized',
- size: 'xl',
- },
- {
- Cell: ({
- row: {
- original: { changed_by_name: changedByName },
- },
- }: any) => changedByName,
- Header: t('Modified By'),
- accessor: 'changed_by.first_name',
- size: 'xl',
- },
- {
- accessor: 'database',
- disableSortBy: true,
- hidden: true,
- },
- {
- Cell: ({
- row: {
- original: { owners, table_name: tableName },
+ accessor: 'kind_icon',
+ disableSortBy: true,
+ size: 'xs',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { table_name: datasetTitle },
+ },
+ }: any) => datasetTitle,
+ Header: t('Name'),
+ accessor: 'table_name',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { kind },
+ },
+ }: any) => kind[0]?.toUpperCase() + kind.slice(1),
+ Header: t('Type'),
+ accessor: 'kind',
+ disableSortBy: true,
+ size: 'md',
+ },
+ {
+ Header: t('Source'),
+ accessor: 'database.database_name',
+ size: 'lg',
+ },
+ {
+ Header: t('Schema'),
+ accessor: 'schema',
+ size: 'lg',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { changed_on_delta_humanized: changedOn },
+ },
+ }: any) => <span className="no-wrap">{changedOn}</span>,
+ Header: t('Modified'),
+ accessor: 'changed_on_delta_humanized',
+ size: 'xl',
+ },
+ {
+ Cell: ({
+ row: {
+ original: { changed_by_name: changedByName },
+ },
+ }: any) => changedByName,
+ Header: t('Modified By'),
+ accessor: 'changed_by.first_name',
+ size: 'xl',
+ },
+ {
+ accessor: 'database',
+ disableSortBy: true,
+ hidden: true,
+ },
+ {
+ Cell: ({
+ row: {
+ original: { owners, table_name: tableName },
+ },
+ }: any) => {
+ if (!owners) {
+ return null;
+ }
+ return owners
+ .slice(0, 5)
+ .map((owner: Owner) => (
+ <AvatarIcon
+ key={owner.id}
+ uniqueKey={`${tableName}-${owner.username}`}
+ firstName={owner.first_name}
+ lastName={owner.last_name}
+ iconSize={24}
+ textSize={9}
+ />
+ ));
},
- }: any) => {
- if (!owners) {
- return null;
- }
- return owners
- .slice(0, 5)
- .map((owner: Owner) => (
- <AvatarIcon
- key={owner.id}
- uniqueKey={`${tableName}-${owner.username}`}
- firstName={owner.first_name}
- lastName={owner.last_name}
- iconSize={24}
- textSize={9}
- />
- ));
+ Header: t('Owners'),
+ id: 'owners',
+ disableSortBy: true,
+ size: 'lg',
},
- Header: t('Owners'),
- id: 'owners',
- disableSortBy: true,
- size: 'lg',
- },
- {
- accessor: 'is_sqllab_view',
- hidden: true,
- disableSortBy: true,
- },
- {
- Cell: ({ row: { original } }: any) => {
- const handleEdit = () => openDatasetEditModal(original);
- const handleDelete = () => openDatasetDeleteModal(original);
- if (!canEdit && !canDelete) {
- return null;
- }
- return (
- <span className="actions">
- <TooltipWrapper
- label="explore-action"
- tooltip={t('Explore')}
- placement="bottom"
- >
- <a
- role="button"
- tabIndex={0}
- className="action-button"
- href={original.explore_url}
- >
- <Icon name="compass" />
- </a>
- </TooltipWrapper>
- {canDelete && (
+ {
+ accessor: 'is_sqllab_view',
+ hidden: true,
+ disableSortBy: true,
+ },
+ {
+ Cell: ({ row: { original } }: any) => {
+ const handleEdit = () => openDatasetEditModal(original);
+ const handleDelete = () => openDatasetDeleteModal(original);
+ if (!canEdit && !canDelete) {
+ return null;
+ }
+ return (
+ <span className="actions">
<TooltipWrapper
- label="delete-action"
- tooltip={t('Delete')}
+ label="explore-action"
+ tooltip={t('Explore')}
placement="bottom"
>
- <span
+ <a
role="button"
tabIndex={0}
className="action-button"
- onClick={handleDelete}
+ href={original.explore_url}
>
- <Icon name="trash" />
- </span>
+ <Icon name="compass" />
+ </a>
</TooltipWrapper>
- )}
+ {canDelete && (
+ <TooltipWrapper
+ label="delete-action"
+ tooltip={t('Delete')}
+ placement="bottom"
+ >
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={handleDelete}
+ >
+ <Icon name="trash" />
+ </span>
+ </TooltipWrapper>
+ )}
- {canEdit && (
- <TooltipWrapper
- label="edit-action"
- tooltip={t('Edit')}
- placement="bottom"
- >
- <span
- role="button"
- tabIndex={0}
- className="action-button"
- onClick={handleEdit}
+ {canEdit && (
+ <TooltipWrapper
+ label="edit-action"
+ tooltip={t('Edit')}
+ placement="bottom"
>
- <Icon name="pencil" />
- </span>
- </TooltipWrapper>
- )}
- </span>
- );
+ <span
+ role="button"
+ tabIndex={0}
+ className="action-button"
+ onClick={handleEdit}
+ >
+ <Icon name="pencil" />
+ </span>
+ </TooltipWrapper>
+ )}
+ </span>
+ );
+ },
+ Header: t('Actions'),
+ id: 'actions',
+ disableSortBy: true,
},
- Header: t('Actions'),
- id: 'actions',
- disableSortBy: true,
- },
- ];
+ ],
+ [canCreate, canEdit, canDelete],
+ );
+
+ const filterTypes: Filters = useMemo(
+ () => [
+ {
+ Header: t('Owner'),
+ id: 'owners',
+ input: 'select',
+ operator: 'rel_m_m',
+ unfilteredLabel: 'All',
+ fetchSelects: createFetchRelated(
+ 'dataset',
+ 'owners',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset owner values: %s',
+ errMsg,
+ ),
+ ),
+ ),
+ paginate: true,
+ },
+ {
+ Header: t('Datasource'),
+ id: 'database',
+ input: 'select',
+ operator: 'rel_o_m',
+ unfilteredLabel: 'All',
+ fetchSelects: createFetchRelated(
+ 'dataset',
+ 'database',
+ createErrorHandler(errMsg =>
+ t(
+ 'An error occurred while fetching dataset datasource values: %s',
+ errMsg,
+ ),
+ ),
+ ),
+ paginate: true,
+ },
+ {
+ Header: t('Schema'),
+ id: 'schema',
+ input: 'select',
+ operator: 'eq',
+ unfilteredLabel: 'All',
+ fetchSelects: createFetchSchemas(errMsg =>
+ t('An error occurred while fetching schema values: %s', errMsg),
+ ),
+ paginate: true,
+ },
+ {
+ Header: t('Type'),
+ id: 'is_sqllab_view',
+ input: 'select',
+ operator: 'eq',
+ unfilteredLabel: 'All',
+ selects: [
+ { label: 'Virtual', value: true },
+ { label: 'Physical', value: false },
+ ],
+ },
+ {
+ Header: t('Search'),
+ id: 'table_name',
+ input: 'search',
+ operator: 'ct',
+ },
+ ],
+ [],
+ );
const menuData: SubMenuProps = {
activeChild: 'Datasets',
@@ -460,7 +439,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
if (canDelete) {
menuData.secondaryButton = {
name: t('Bulk Select'),
- onClick: () => setBulkSelectEnabled(!bulkSelectEnabled),
+ onClick: toggleBulkSelect,
};
}
@@ -468,60 +447,16 @@ const DatasetList: FunctionComponent<DatasetListProps> =
({
setDatasetCurrentlyDeleting(null);
};
- const closeDatasetEditModal = () => setDatasetCurrentlyEditing(null);
-
- const fetchData = useCallback(
- ({ pageIndex, pageSize, sortBy, filters }: FetchDataConfig) => {
- // set loading state, cache the last config for fetching data in this
component.
- setLoading(true);
- setLastFetchDataConfig({
- filters,
- pageIndex,
- pageSize,
- sortBy,
- });
- const filterExps = filters.map(({ id: col, operator: opr, value }) => ({
- col,
- opr,
- value,
- }));
-
- const queryParams = rison.encode({
- order_column: sortBy[0].id,
- order_direction: sortBy[0].desc ? 'desc' : 'asc',
- page: pageIndex,
- page_size: pageSize,
- ...(filterExps.length ? { filters: filterExps } : {}),
- });
-
- return SupersetClient.get({
- endpoint: `/api/v1/dataset/?q=${queryParams}`,
- })
- .then(
- ({ json }) => {
- setLoading(false);
- setDatasets(json.result);
- setDatasetCount(json.count);
- },
- createErrorHandler(errMsg =>
- addDangerToast(
- t('An error occurred while fetching datasets: %s', errMsg),
- ),
- ),
- )
- .finally(() => setLoading(false));
- },
- [],
- );
+ const closeDatasetEditModal = () => {
+ setDatasetCurrentlyEditing(null);
+ };
const handleDatasetDelete = ({ id, table_name: tableName }: Dataset) => {
SupersetClient.delete({
endpoint: `/api/v1/dataset/${id}`,
}).then(
() => {
- if (lastFetchDataConfig) {
- fetchData(lastFetchDataConfig);
- }
+ refreshData();
setDatasetCurrentlyDeleting(null);
addSuccessToast(t('Deleted: %s', tableName));
},
@@ -533,16 +468,14 @@ const DatasetList: FunctionComponent<DatasetListProps> =
({
);
};
- const handleBulkDatasetDelete = () => {
+ const handleBulkDatasetDelete = (datasetsToDelete: Dataset[]) => {
SupersetClient.delete({
endpoint: `/api/v1/dataset/?q=${rison.encode(
- datasets.map(({ id }) => id),
+ datasetsToDelete.map(({ id }) => id),
)}`,
}).then(
({ json = {} }) => {
- if (lastFetchDataConfig) {
- fetchData(lastFetchDataConfig);
- }
+ refreshData();
addSuccessToast(json.message);
},
createErrorHandler(errMsg =>
@@ -553,21 +486,13 @@ const DatasetList: FunctionComponent<DatasetListProps> =
({
);
};
- const handleUpdateDataset = () => {
- if (lastFetchDataConfig) {
- fetchData(lastFetchDataConfig);
- }
- };
-
return (
<>
<SubMenu {...menuData} />
<AddDatasetModal
show={datasetAddModalOpen}
onHide={() => setDatasetAddModalOpen(false)}
- onDatasetAdd={() => {
- if (lastFetchDataConfig) fetchData(lastFetchDataConfig);
- }}
+ onDatasetAdd={refreshData}
/>
{datasetCurrentlyDeleting && (
<DeleteModal
@@ -577,12 +502,24 @@ const DatasetList: FunctionComponent<DatasetListProps> =
({
datasetCurrentlyDeleting.chart_count,
datasetCurrentlyDeleting.dashboard_count,
)}
- onConfirm={() => handleDatasetDelete(datasetCurrentlyDeleting)}
+ onConfirm={() => {
+ if (datasetCurrentlyDeleting) {
+ handleDatasetDelete(datasetCurrentlyDeleting);
+ }
+ }}
onHide={closeDatasetDeleteModal}
open
title={t('Delete Dataset?')}
/>
)}
+ {datasetCurrentlyEditing && (
+ <DatasourceModal
+ datasource={datasetCurrentlyEditing}
+ onDatasourceSave={refreshData}
+ onHide={closeDatasetEditModal}
+ show
+ />
+ )}
<ConfirmStatusChange
title={t('Please confirm')}
description={t(
@@ -603,82 +540,54 @@ const DatasetList: FunctionComponent<DatasetListProps> =
({
: [];
return (
- <>
- {datasetCurrentlyDeleting && (
- <DeleteModal
- description={t(
- `The dataset ${datasetCurrentlyDeleting.table_name} is
linked to
- ${datasetCurrentlyDeleting.chart_count} charts that appear on
- ${datasetCurrentlyDeleting.dashboard_count} dashboards.
- Are you sure you want to continue? Deleting the dataset will
break
- those objects.`,
- )}
- onConfirm={() =>
- handleDatasetDelete(datasetCurrentlyDeleting)
- }
- onHide={closeDatasetDeleteModal}
- open
- title={t('Delete Dataset?')}
- />
- )}
- {datasetCurrentlyEditing && (
- <DatasourceModal
- datasource={datasetCurrentlyEditing}
- onDatasourceSave={handleUpdateDataset}
- onHide={closeDatasetEditModal}
- show
- />
- )}
- <ListView
- className="dataset-list-view"
- columns={columns}
- data={datasets}
- count={datasetCount}
- pageSize={PAGE_SIZE}
- fetchData={fetchData}
- filters={filterTypes}
- loading={loading}
- initialSort={initialSort}
- bulkActions={bulkActions}
- bulkSelectEnabled={bulkSelectEnabled}
- disableBulkSelect={() => setBulkSelectEnabled(false)}
- renderBulkSelectCopy={selected => {
- const { virtualCount, physicalCount } = selected.reduce(
- (acc, e) => {
- if (e.original.kind === 'physical')
- acc.physicalCount += 1;
- else if (e.original.kind === 'virtual')
- acc.virtualCount += 1;
- return acc;
- },
- { virtualCount: 0, physicalCount: 0 },
+ <ListView
+ className="dataset-list-view"
+ columns={columns}
+ data={datasets}
+ count={datasetCount}
+ pageSize={PAGE_SIZE}
+ fetchData={fetchData}
+ filters={filterTypes}
+ loading={loading}
+ initialSort={initialSort}
+ bulkActions={bulkActions}
+ bulkSelectEnabled={bulkSelectEnabled}
+ disableBulkSelect={toggleBulkSelect}
+ renderBulkSelectCopy={selected => {
+ const { virtualCount, physicalCount } = selected.reduce(
+ (acc, e) => {
+ if (e.original.kind === 'physical') acc.physicalCount += 1;
+ else if (e.original.kind === 'virtual')
+ acc.virtualCount += 1;
+ return acc;
+ },
+ { virtualCount: 0, physicalCount: 0 },
+ );
+
+ if (!selected.length) {
+ return t('0 Selected');
+ } else if (virtualCount && !physicalCount) {
+ return t(
+ '%s Selected (Virtual)',
+ selected.length,
+ virtualCount,
);
-
- if (!selected.length) {
- return t('0 Selected');
- } else if (virtualCount && !physicalCount) {
- return t(
- '%s Selected (Virtual)',
- selected.length,
- virtualCount,
- );
- } else if (physicalCount && !virtualCount) {
- return t(
- '%s Selected (Physical)',
- selected.length,
- physicalCount,
- );
- }
-
+ } else if (physicalCount && !virtualCount) {
return t(
- '%s Selected (%s Physical, %s Virtual)',
+ '%s Selected (Physical)',
selected.length,
physicalCount,
- virtualCount,
);
- }}
- />
- </>
+ }
+
+ return t(
+ '%s Selected (%s Physical, %s Virtual)',
+ selected.length,
+ physicalCount,
+ virtualCount,
+ );
+ }}
+ />
);
}}
</ConfirmStatusChange>
diff --git a/superset-frontend/src/views/CRUD/hooks.ts
b/superset-frontend/src/views/CRUD/hooks.ts
new file mode 100644
index 0000000..56e5c56
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/hooks.ts
@@ -0,0 +1,224 @@
+/**
+ * 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 rison from 'rison';
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { SupersetClient } from '@superset-ui/connection';
+import { t } from '@superset-ui/translation';
+
+import { createErrorHandler } from 'src/views/CRUD/utils';
+import { FetchDataConfig } from 'src/components/ListView';
+import { FavoriteStatus } from './types';
+
+interface ListViewResourceState<D extends object = any> {
+ loading: boolean;
+ collection: D[];
+ count: number;
+ permissions: string[];
+ lastFetchDataConfig: FetchDataConfig | null;
+ bulkSelectEnabled: boolean;
+}
+
+export function useListViewResource<D extends object = any>(
+ resource: string,
+ resourceLabel: string, // resourceLabel for translations
+ handleErrorMsg: (errorMsg: string) => void,
+) {
+ const [state, setState] = useState<ListViewResourceState<D>>({
+ count: 0,
+ collection: [],
+ loading: true,
+ lastFetchDataConfig: null,
+ permissions: [],
+ bulkSelectEnabled: false,
+ });
+
+ function updateState(update: Partial<ListViewResourceState<D>>) {
+ setState(currentState => ({ ...currentState, ...update }));
+ }
+
+ function toggleBulkSelect() {
+ updateState({ bulkSelectEnabled: !state.bulkSelectEnabled });
+ }
+
+ useEffect(() => {
+ SupersetClient.get({
+ endpoint: `/api/v1/${resource}/_info`,
+ }).then(
+ ({ json: infoJson = {} }) => {
+ updateState({
+ permissions: infoJson.permissions,
+ });
+ },
+ createErrorHandler(errMsg =>
+ handleErrorMsg(
+ t(
+ 'An error occurred while fetching %ss info: %s',
+ resourceLabel,
+ errMsg,
+ ),
+ ),
+ ),
+ );
+ }, []);
+
+ function hasPerm(perm: string) {
+ if (!state.permissions.length) {
+ return false;
+ }
+
+ return Boolean(state.permissions.find(p => p === perm));
+ }
+
+ const fetchData = useCallback(
+ ({
+ pageIndex,
+ pageSize,
+ sortBy,
+ filters: filterValues,
+ }: FetchDataConfig) => {
+ // set loading state, cache the last config for refreshing data.
+ updateState({
+ lastFetchDataConfig: {
+ filters: filterValues,
+ pageIndex,
+ pageSize,
+ sortBy,
+ },
+ loading: true,
+ });
+
+ const filterExps = filterValues.map(
+ ({ id: col, operator: opr, value }) => ({
+ col,
+ opr,
+ value,
+ }),
+ );
+
+ const queryParams = rison.encode({
+ order_column: sortBy[0].id,
+ order_direction: sortBy[0].desc ? 'desc' : 'asc',
+ page: pageIndex,
+ page_size: pageSize,
+ ...(filterExps.length ? { filters: filterExps } : {}),
+ });
+
+ return SupersetClient.get({
+ endpoint: `/api/v1/${resource}/?q=${queryParams}`,
+ })
+ .then(
+ ({ json = {} }) => {
+ updateState({
+ collection: json.result,
+ count: json.count,
+ });
+ },
+ createErrorHandler(errMsg =>
+ handleErrorMsg(
+ t(
+ 'An error occurred while fetching %ss: %s',
+ resourceLabel,
+ errMsg,
+ ),
+ ),
+ ),
+ )
+ .finally(() => {
+ updateState({ loading: false });
+ });
+ },
+ [],
+ );
+
+ return {
+ state: {
+ loading: state.loading,
+ resourceCount: state.count,
+ resourceCollection: state.collection,
+ bulkSelectEnabled: state.bulkSelectEnabled,
+ },
+ setResourceCollection: (update: D[]) =>
+ updateState({
+ collection: update,
+ }),
+ hasPerm,
+ fetchData,
+ toggleBulkSelect,
+ refreshData: () => {
+ if (state.lastFetchDataConfig) {
+ fetchData(state.lastFetchDataConfig);
+ }
+ },
+ };
+}
+
+// the hooks api has some known limitations around stale state in closures.
+// See
https://github.com/reactjs/rfcs/blob/master/text/0068-react-hooks.md#drawbacks
+// the useRef hook is a way of getting around these limitations by having a
consistent ref
+// that points to the most recent value.
+export function useFavoriteStatus(
+ initialState: FavoriteStatus,
+ baseURL: string,
+ handleErrorMsg: (message: string) => void,
+) {
+ const [favoriteStatus, setFavoriteStatus] = useState<FavoriteStatus>(
+ initialState,
+ );
+ const favoriteStatusRef = useRef<FavoriteStatus>(favoriteStatus);
+ useEffect(() => {
+ favoriteStatusRef.current = favoriteStatus;
+ });
+
+ const updateFavoriteStatus = (update: FavoriteStatus) =>
+ setFavoriteStatus(currentState => ({ ...currentState, ...update }));
+
+ const fetchFaveStar = (id: number) => {
+ SupersetClient.get({
+ endpoint: `${baseURL}/${id}/count/`,
+ }).then(
+ ({ json }) => {
+ updateFavoriteStatus({ [id]: json.count > 0 });
+ },
+ createErrorHandler(errMsg =>
+ handleErrorMsg(
+ t('There was an error fetching the favorite status: %s', errMsg),
+ ),
+ ),
+ );
+ };
+
+ const saveFaveStar = (id: number, isStarred: boolean) => {
+ const urlSuffix = isStarred ? 'unselect' : 'select';
+
+ SupersetClient.get({
+ endpoint: `${baseURL}/${id}/${urlSuffix}/`,
+ }).then(
+ () => {
+ updateFavoriteStatus({ [id]: !isStarred });
+ },
+ createErrorHandler(errMsg =>
+ handleErrorMsg(
+ t('There was an error saving the favorite status: %s', errMsg),
+ ),
+ ),
+ );
+ };
+
+ return [favoriteStatusRef, fetchFaveStar, saveFaveStar] as const;
+}
diff --git a/superset-frontend/src/views/CRUD/types.ts
b/superset-frontend/src/views/CRUD/types.ts
new file mode 100644
index 0000000..91d88a3
--- /dev/null
+++ b/superset-frontend/src/views/CRUD/types.ts
@@ -0,0 +1,22 @@
+/**
+ * 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.
+ */
+
+export type FavoriteStatus = {
+ [id: number]: boolean;
+};
diff --git a/superset-frontend/src/views/CRUD/utils.tsx
b/superset-frontend/src/views/CRUD/utils.tsx
index 5aea548..2bced46 100644
--- a/superset-frontend/src/views/CRUD/utils.tsx
+++ b/superset-frontend/src/views/CRUD/utils.tsx
@@ -20,7 +20,6 @@ import {
SupersetClient,
SupersetClientResponse,
} from '@superset-ui/connection';
-import { t } from '@superset-ui/translation';
import rison from 'rison';
import getClientErrorObject from 'src/utils/getClientErrorObject';
import { logging } from '@superset-ui/core';
@@ -58,59 +57,6 @@ export function createErrorHandler(handleErrorFunc:
(errMsg?: string) => void) {
return async (e: SupersetClientResponse | string) => {
const parsedError = await getClientErrorObject(e);
logging.error(e);
- handleErrorFunc(parsedError.message);
- };
-}
-
-export function createFaveStarHandlers(
- baseURL: string,
- context: any,
- handleErrorFunc: (message: string) => void,
-) {
- const fetchFaveStar = (id: number) => {
- SupersetClient.get({
- endpoint: `${baseURL}/${id}/count/`,
- })
- .then(({ json }) => {
- const faves = {
- ...context.state.favoriteStatus,
- };
-
- faves[id] = json.count > 0;
-
- context.setState({
- favoriteStatus: faves,
- });
- })
- .catch(() =>
- handleErrorFunc(t('There was an error fetching the favorite status')),
- );
- };
-
- const saveFaveStar = (id: number, isStarred: boolean) => {
- const urlSuffix = isStarred ? 'unselect' : 'select';
-
- SupersetClient.get({
- endpoint: `${baseURL}/${id}/${urlSuffix}/`,
- })
- .then(() => {
- const faves = {
- ...context.state.favoriteStatus,
- };
-
- faves[id] = !isStarred;
-
- context.setState({
- favoriteStatus: faves,
- });
- })
- .catch(() =>
- handleErrorFunc(t('There was an error saving the favorite status')),
- );
- };
-
- return {
- fetchFaveStar,
- saveFaveStar,
+ handleErrorFunc(parsedError.error);
};
}