This is an automated email from the ASF dual-hosted git repository. villebro pushed a commit to branch 0.38 in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
commit b3c7740b96c063bb6c580768455cd40511052967 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); }; }
