refactor: useListViewResource hook for charts, dashboards, datasets (#10680)
This commit is contained in:
parent
bc0fc4ea25
commit
6ff96cfc72
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/chart/_info`,
|
||||
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(
|
||||
({ json: infoJson = {} }) => {
|
||||
this.setState({
|
||||
permissions: infoJson.permissions,
|
||||
});
|
||||
() => {
|
||||
refreshData();
|
||||
props.addSuccessToast(t('Deleted: %s', sliceName));
|
||||
},
|
||||
() => {
|
||||
props.addDangerToast(t('There was an issue deleting: %s', sliceName));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function handleBulkChartDelete(chartsToDelete: Chart[]) {
|
||||
SupersetClient.delete({
|
||||
endpoint: `/api/v1/chart/?q=${rison.encode(
|
||||
chartsToDelete.map(({ id }) => id),
|
||||
)}`,
|
||||
}).then(
|
||||
({ 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');
|
||||
function renderFaveStar(id: number) {
|
||||
return (
|
||||
<FaveStar
|
||||
itemId={id}
|
||||
fetchFaveStar={fetchFaveStar}
|
||||
saveFaveStar={saveFaveStar}
|
||||
isStarred={!!favoriteStatusRef.current[id]}
|
||||
height={20}
|
||||
width={20}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
get canDelete() {
|
||||
return this.hasPerm('can_delete');
|
||||
}
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { id },
|
||||
},
|
||||
}: any) => renderFaveStar(id),
|
||||
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,
|
||||
},
|
||||
},
|
||||
}: 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;
|
||||
}
|
||||
|
||||
initialSort = [{ id: 'changed_on_delta_humanized', desc: true }];
|
||||
|
||||
fetchMethods = createFaveStarHandlers(
|
||||
FAVESTAR_BASE_URL,
|
||||
this,
|
||||
(message: string) => {
|
||||
this.props.addDangerToast(message);
|
||||
},
|
||||
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,
|
||||
},
|
||||
],
|
||||
[canEdit, canDelete, favoriteStatusRef],
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}: 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;
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
},
|
||||
Header: t('Actions'),
|
||||
id: 'actions',
|
||||
disableSortBy: true,
|
||||
},
|
||||
];
|
||||
|
||||
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
|
||||
}
|
||||
/>
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu
|
||||
name={t('Charts')}
|
||||
secondaryButton={
|
||||
canDelete
|
||||
? {
|
||||
name: t('Bulk Select'),
|
||||
onClick: toggleBulkSelect,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{sliceCurrentlyEditing && (
|
||||
<PropertiesModal
|
||||
onHide={closeChartEditModal}
|
||||
onSave={handleChartUpdated}
|
||||
show
|
||||
slice={sliceCurrentlyEditing}
|
||||
/>
|
||||
)}
|
||||
<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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
||||
state: State = {
|
||||
bulkSelectEnabled: false,
|
||||
dashboardCount: 0,
|
||||
dashboards: [],
|
||||
favoriteStatus: {}, // Hash mapping dashboard id to 'isStarred' status
|
||||
dashboardToEdit: null,
|
||||
lastFetchDataConfig: null,
|
||||
loading: true,
|
||||
permissions: [],
|
||||
};
|
||||
const [dashboardToEdit, setDashboardToEdit] = useState<Dashboard | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
componentDidMount() {
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/dashboard/_info`,
|
||||
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);
|
||||
}
|
||||
|
||||
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 }];
|
||||
function renderFaveStar(id: number) {
|
||||
return (
|
||||
<FaveStar
|
||||
itemId={id}
|
||||
fetchFaveStar={fetchFaveStar}
|
||||
saveFaveStar={saveFaveStar}
|
||||
isStarred={!!favoriteStatusRef.current[id]}
|
||||
height={20}
|
||||
width={20}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
fetchMethods = createFaveStarHandlers(
|
||||
FAVESTAR_BASE_URL,
|
||||
this,
|
||||
(message: string) => {
|
||||
this.props.addDangerToast(message);
|
||||
},
|
||||
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,
|
||||
},
|
||||
{
|
||||
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>
|
||||
);
|
||||
},
|
||||
Header: t('Actions'),
|
||||
id: 'actions',
|
||||
disableSortBy: true,
|
||||
},
|
||||
],
|
||||
[canEdit, canDelete, canExport, favoriteStatusRef],
|
||||
);
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
}: 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 });
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
/>
|
||||
<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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ConfirmStatusChange>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
<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);
|
||||
|
|
|
|||
|
|
@ -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 [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 {
|
||||
state: {
|
||||
loading,
|
||||
resourceCount: datasetCount,
|
||||
resourceCollection: datasets,
|
||||
bulkSelectEnabled,
|
||||
},
|
||||
hasPerm,
|
||||
fetchData,
|
||||
toggleBulkSelect,
|
||||
refreshData,
|
||||
} = useListViewResource<Dataset>('dataset', t('dataset'), addDangerToast);
|
||||
|
||||
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 [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
|
||||
(Dataset & { chart_count: number; dashboard_count: number }) | null
|
||||
>(null);
|
||||
|
||||
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 [
|
||||
datasetCurrentlyEditing,
|
||||
setDatasetCurrentlyEditing,
|
||||
] = useState<Dataset | null>(null);
|
||||
|
||||
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',
|
||||
},
|
||||
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}
|
||||
/>
|
||||
));
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
original: { table_name: datasetTitle },
|
||||
},
|
||||
}: any) => datasetTitle,
|
||||
Header: t('Name'),
|
||||
accessor: 'table_name',
|
||||
},
|
||||
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 && (
|
||||
{
|
||||
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}
|
||||
/>
|
||||
));
|
||||
},
|
||||
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="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 },
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
<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 (%s Physical, %s Virtual)',
|
||||
'%s Selected (Virtual)',
|
||||
selected.length,
|
||||
physicalCount,
|
||||
virtualCount,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
} else if (physicalCount && !virtualCount) {
|
||||
return t(
|
||||
'%s Selected (Physical)',
|
||||
selected.length,
|
||||
physicalCount,
|
||||
);
|
||||
}
|
||||
|
||||
return t(
|
||||
'%s Selected (%s Physical, %s Virtual)',
|
||||
selected.length,
|
||||
physicalCount,
|
||||
virtualCount,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ConfirmStatusChange>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue