{
)}
- {this.canExport && (
+ {canExport && (
this.handleBulkDashboardExport([props])}
+ onClick={() => handleBulkDashboardExport([dashboard])}
>
Export
)}
- {this.canEdit && (
+ {canEdit && (
this.openDashboardEditModal(props)}
+ onClick={() => openDashboardEditModal(dashboard)}
>
Edit
@@ -528,17 +428,22 @@ class DashboardList extends React.PureComponent
{
return (
{props.published ? 'published' : 'draft'}}
- url={this.state.bulkSelectEnabled ? undefined : props.url}
- imgURL={props.thumbnail_url}
+ loading={dashboard.loading}
+ title={dashboard.dashboard_title}
+ titleRight={
+
+ }
+ 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 => (
{
))}
actions={
-
+ {renderFaveStar(dashboard.id)}
@@ -562,87 +460,78 @@ class DashboardList extends React.PureComponent {
}
/>
);
- };
+ }
- render() {
- const {
- bulkSelectEnabled,
- dashboards,
- dashboardCount,
- loading,
- dashboardToEdit,
- } = this.state;
- return (
- <>
-
+
+
+ {confirmDelete => {
+ const bulkActions: ListViewProps['bulkActions'] = [];
+ if (canDelete) {
+ bulkActions.push({
+ key: 'delete',
+ name: t('Delete'),
+ type: 'danger',
+ onSelect: confirmDelete,
+ });
}
- />
-
- {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 && (
- this.setState({ dashboardToEdit: null })}
- onSubmit={this.handleDashboardEdit}
- />
- )}
-
+ {dashboardToEdit && (
+ setDashboardToEdit(null)}
+ onSubmit={handleDashboardEdit}
/>
- >
- );
- }}
-
- >
- );
- }
+ )}
+
+ >
+ );
+ }}
+
+ >
+ );
}
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 f7a5ab04f5aff..668739c5f72f6 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;
@@ -104,118 +97,31 @@ const DatasetList: FunctionComponent = ({
addDangerToast,
addSuccessToast,
}) => {
- const [datasetCount, setDatasetCount] = useState(0);
+ const {
+ state: {
+ loading,
+ resourceCount: datasetCount,
+ resourceCollection: datasets,
+ bulkSelectEnabled,
+ },
+ hasPerm,
+ fetchData,
+ toggleBulkSelect,
+ refreshData,
+ } = useListViewResource('dataset', t('dataset'), addDangerToast);
+
+ const [datasetAddModalOpen, setDatasetAddModalOpen] = useState(
+ false,
+ );
+
const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
(Dataset & { chart_count: number; dashboard_count: number }) | null
>(null);
+
const [
datasetCurrentlyEditing,
setDatasetCurrentlyEditing,
] = useState(null);
- const [datasets, setDatasets] = useState([]);
- const [
- lastFetchDataConfig,
- setLastFetchDataConfig,
- ] = useState(null);
- const [loading, setLoading] = useState(true);
- const [permissions, setPermissions] = useState([]);
-
- const [datasetAddModalOpen, setDatasetAddModalOpen] = useState(
- false,
- );
- const [bulkSelectEnabled, setBulkSelectEnabled] = useState(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 = ({
),
);
- const columns = [
- {
- Cell: ({
- row: {
- original: { kind },
- },
- }: any) => {
- if (kind === 'physical')
+ const columns = useMemo(
+ () => [
+ {
+ Cell: ({
+ row: {
+ original: { kind },
+ },
+ }: any) => {
+ if (kind === 'physical')
+ return (
+
+
+
+ );
+
return (
-
+
);
-
- return (
-
-
-
- );
- },
- 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) => {changedOn},
- 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) => {changedOn},
+ 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) => (
+
+ ));
},
- }: any) => {
- if (!owners) {
- return null;
- }
- return owners
- .slice(0, 5)
- .map((owner: Owner) => (
-
- ));
+ 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 (
-
-
-
-
-
-
- {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 (
+
-
-
-
+
+
- )}
+ {canDelete && (
+
+
+
+
+
+ )}
- {canEdit && (
-
-
-
-
-
- )}
-
- );
+
+
+
+
+ )}
+
+ );
+ },
+ 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 = ({
if (canDelete) {
menuData.secondaryButton = {
name: t('Bulk Select'),
- onClick: () => setBulkSelectEnabled(!bulkSelectEnabled),
+ onClick: toggleBulkSelect,
};
}
@@ -468,60 +447,16 @@ const DatasetList: FunctionComponent = ({
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 = ({
);
};
- 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 = ({
);
};
- const handleUpdateDataset = () => {
- if (lastFetchDataConfig) {
- fetchData(lastFetchDataConfig);
- }
- };
-
return (
<>
setDatasetAddModalOpen(false)}
- onDatasetAdd={() => {
- if (lastFetchDataConfig) fetchData(lastFetchDataConfig);
- }}
+ onDatasetAdd={refreshData}
/>
{datasetCurrentlyDeleting && (
= ({
datasetCurrentlyDeleting.chart_count,
datasetCurrentlyDeleting.dashboard_count,
)}
- onConfirm={() => handleDatasetDelete(datasetCurrentlyDeleting)}
+ onConfirm={() => {
+ if (datasetCurrentlyDeleting) {
+ handleDatasetDelete(datasetCurrentlyDeleting);
+ }
+ }}
onHide={closeDatasetDeleteModal}
open
title={t('Delete Dataset?')}
/>
)}
+ {datasetCurrentlyEditing && (
+
+ )}
= ({
: [];
return (
- <>
- {datasetCurrentlyDeleting && (
-
- handleDatasetDelete(datasetCurrentlyDeleting)
- }
- onHide={closeDatasetDeleteModal}
- open
- title={t('Delete Dataset?')}
- />
- )}
- {datasetCurrentlyEditing && (
-
- )}
- 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 },
+ {
+ 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,
+ );
+ }}
+ />
);
}}
diff --git a/superset-frontend/src/views/CRUD/hooks.ts b/superset-frontend/src/views/CRUD/hooks.ts
new file mode 100644
index 0000000000000..56e5c56443039
--- /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 {
+ loading: boolean;
+ collection: D[];
+ count: number;
+ permissions: string[];
+ lastFetchDataConfig: FetchDataConfig | null;
+ bulkSelectEnabled: boolean;
+}
+
+export function useListViewResource(
+ resource: string,
+ resourceLabel: string, // resourceLabel for translations
+ handleErrorMsg: (errorMsg: string) => void,
+) {
+ const [state, setState] = useState>({
+ count: 0,
+ collection: [],
+ loading: true,
+ lastFetchDataConfig: null,
+ permissions: [],
+ bulkSelectEnabled: false,
+ });
+
+ function updateState(update: Partial>) {
+ 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(
+ initialState,
+ );
+ const favoriteStatusRef = useRef(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 0000000000000..91d88a375e547
--- /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 5aea548a78a82..2bced467bb3c3 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);
};
}