mirror of https://github.com/apache/superset.git
feat: update delete modal for dataset (#10258)
* update delete modal for dataset * update datasetList to use hooks * fix typo on dataset delete modal
This commit is contained in:
parent
4d179622fa
commit
80b06f6827
|
@ -18,8 +18,10 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
import { Modal, Button } from 'react-bootstrap';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { supersetTheme, ThemeProvider } from '@superset-ui/style';
|
||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||
import Modal from 'src/components/Modal';
|
||||
|
||||
describe('ConfirmStatusChange', () => {
|
||||
const mockedProps = {
|
||||
|
@ -35,6 +37,10 @@ describe('ConfirmStatusChange', () => {
|
|||
</>
|
||||
)}
|
||||
</ConfirmStatusChange>,
|
||||
{
|
||||
wrappingComponent: ThemeProvider,
|
||||
wrappingComponentProps: { theme: supersetTheme },
|
||||
},
|
||||
);
|
||||
|
||||
it('opens a confirm modal', () => {
|
||||
|
|
|
@ -16,63 +16,55 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/translation';
|
||||
import * as React from 'react';
|
||||
// @ts-ignore
|
||||
import { Button, Modal } from 'react-bootstrap';
|
||||
import React, { useState } from 'react';
|
||||
import DeleteModal from 'src/components/DeleteModal';
|
||||
|
||||
type Callback = (...args: any[]) => void;
|
||||
interface Props {
|
||||
title: string | React.ReactNode;
|
||||
description: string | React.ReactNode;
|
||||
interface ConfirmStatusChangeProps {
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
onConfirm: Callback;
|
||||
children: (showConfirm: Callback) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
callbackArgs: any[];
|
||||
open: boolean;
|
||||
}
|
||||
export default class ConfirmStatusChange extends React.Component<Props, State> {
|
||||
public state = {
|
||||
callbackArgs: [],
|
||||
open: false,
|
||||
};
|
||||
export default function ConfirmStatusChange({
|
||||
title,
|
||||
description,
|
||||
onConfirm,
|
||||
children,
|
||||
}: ConfirmStatusChangeProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [currentCallbackArgs, setCurrentCallbackArgs] = useState<any[]>([]);
|
||||
|
||||
public showConfirm = (...callbackArgs: any[]) => {
|
||||
const showConfirm = (...callbackArgs: any[]) => {
|
||||
// check if any args are DOM events, if so, call persist
|
||||
callbackArgs.forEach(
|
||||
arg => arg && typeof arg.persist === 'function' && arg.persist(),
|
||||
);
|
||||
|
||||
this.setState({
|
||||
callbackArgs,
|
||||
open: true,
|
||||
});
|
||||
setOpen(true);
|
||||
setCurrentCallbackArgs(callbackArgs);
|
||||
};
|
||||
|
||||
public hide = () => this.setState({ open: false, callbackArgs: [] });
|
||||
|
||||
public confirm = () => {
|
||||
this.props.onConfirm(...this.state.callbackArgs);
|
||||
this.hide();
|
||||
const hide = () => {
|
||||
setOpen(false);
|
||||
setCurrentCallbackArgs([]);
|
||||
};
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
{this.props.children && this.props.children(this.showConfirm)}
|
||||
<Modal show={this.state.open} onHide={this.hide}>
|
||||
<Modal.Header closeButton>{this.props.title}</Modal.Header>
|
||||
<Modal.Body>{this.props.description}</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={this.hide}>{t('Cancel')}</Button>
|
||||
<Button bsStyle="danger" onClick={this.confirm}>
|
||||
{t('OK')}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
const confirm = () => {
|
||||
onConfirm(...currentCallbackArgs);
|
||||
hide();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{children && children(showConfirm)}
|
||||
<DeleteModal
|
||||
description={description}
|
||||
onConfirm={confirm}
|
||||
onHide={hide}
|
||||
open={open}
|
||||
title={title}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* 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 { t } from '@superset-ui/translation';
|
||||
import React, { useState } from 'react';
|
||||
import styled from '@superset-ui/style';
|
||||
import { FormGroup, FormControl } from 'react-bootstrap';
|
||||
import Modal from 'src/components/Modal';
|
||||
|
||||
const StyleFormGroup = styled(FormGroup)`
|
||||
padding-top: 8px;
|
||||
width: 50%;
|
||||
label {
|
||||
color: ${({ theme }) => theme.colors.grayscale.light1};
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`;
|
||||
|
||||
const DescriptionContainer = styled.div`
|
||||
line-height: 40px;
|
||||
padding-top: 16px;
|
||||
`;
|
||||
|
||||
interface DeleteModalProps {
|
||||
description: React.ReactNode;
|
||||
onConfirm: () => void;
|
||||
onHide: () => void;
|
||||
open: boolean;
|
||||
title: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function DeleteModal({
|
||||
description,
|
||||
onConfirm,
|
||||
onHide,
|
||||
open,
|
||||
title,
|
||||
}: DeleteModalProps) {
|
||||
const [disableChange, setDisableChange] = useState(true);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
disablePrimaryButton={disableChange}
|
||||
onHide={onHide}
|
||||
onHandledPrimaryAction={onConfirm}
|
||||
primaryButtonName={t('delete')}
|
||||
primaryButtonType="danger"
|
||||
show={open}
|
||||
title={title}
|
||||
>
|
||||
<DescriptionContainer>{description}</DescriptionContainer>
|
||||
<StyleFormGroup>
|
||||
<label htmlFor="delete">{t('type delete to confirm')}</label>
|
||||
<FormControl
|
||||
id="delete"
|
||||
type="text"
|
||||
// @ts-ignore
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setDisableChange(event.target.value !== 'DELETE')
|
||||
}
|
||||
/>
|
||||
</StyleFormGroup>
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -20,13 +20,15 @@ import React from 'react';
|
|||
import styled from '@superset-ui/style';
|
||||
import { Modal as BaseModal } from 'react-bootstrap';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import Button from './Button';
|
||||
import Button from '../views/datasetList/Button';
|
||||
|
||||
interface ModalProps {
|
||||
children: React.ReactNode;
|
||||
disableSave: boolean;
|
||||
disablePrimaryButton?: boolean;
|
||||
onHide: () => void;
|
||||
onSave: () => void;
|
||||
onHandledPrimaryAction: () => void;
|
||||
primaryButtonName: string;
|
||||
primaryButtonType?: 'primary' | 'danger';
|
||||
show: boolean;
|
||||
title: React.ReactNode;
|
||||
}
|
||||
|
@ -45,8 +47,7 @@ const StyledModal = styled(BaseModal)`
|
|||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 18px 0 340px 18px;
|
||||
width: 65%;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
|
@ -66,9 +67,11 @@ const Title = styled.div`
|
|||
|
||||
export default function Modal({
|
||||
children,
|
||||
disableSave,
|
||||
disablePrimaryButton = false,
|
||||
onHandledPrimaryAction,
|
||||
onHide,
|
||||
onSave,
|
||||
primaryButtonName,
|
||||
primaryButtonType = 'primary',
|
||||
show,
|
||||
title,
|
||||
}: ModalProps) {
|
||||
|
@ -83,8 +86,12 @@ export default function Modal({
|
|||
<BaseModal.Footer>
|
||||
<span className="float-right">
|
||||
<Button onClick={onHide}>{t('Cancel')}</Button>
|
||||
<Button bsStyle="primary" disabled={disableSave} onClick={onSave}>
|
||||
{t('Add')}
|
||||
<Button
|
||||
bsStyle={primaryButtonType}
|
||||
disabled={disablePrimaryButton}
|
||||
onClick={onHandledPrimaryAction}
|
||||
>
|
||||
{primaryButtonName}
|
||||
</Button>
|
||||
</span>
|
||||
</BaseModal.Footer>
|
|
@ -25,7 +25,7 @@ interface ModalProps {
|
|||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
padding?: number;
|
||||
bsStyle?: 'default' | 'primary';
|
||||
bsStyle?: 'default' | 'primary' | 'danger';
|
||||
width?: number;
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,11 @@ const StyledButton = styled(BaseButton)`
|
|||
background-color: ${({ theme }) => theme.colors.primary.base};
|
||||
color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
}
|
||||
&.btn-danger,
|
||||
&.btn-danger:hover {
|
||||
background-color: ${({ theme }) => theme.colors.error.base};
|
||||
color: ${({ theme }) => theme.colors.grayscale.light5};
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Modal({
|
||||
|
|
|
@ -19,13 +19,18 @@
|
|||
import { SupersetClient } from '@superset-ui/connection';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import rison from 'rison';
|
||||
// @ts-ignore
|
||||
import { Panel } from 'react-bootstrap';
|
||||
import { SHORT_DATE, SHORT_TIME } from 'src/utils/common';
|
||||
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
|
||||
import DeleteModal from 'src/components/DeleteModal';
|
||||
import ListView from 'src/components/ListView/ListView';
|
||||
import SubMenu from 'src/components/Menu/SubMenu';
|
||||
import AvatarIcon from 'src/components/AvatarIcon';
|
||||
|
@ -41,29 +46,17 @@ import Icon from 'src/components/Icon';
|
|||
const PAGE_SIZE = 25;
|
||||
|
||||
type Owner = {
|
||||
id: string;
|
||||
first_name: string;
|
||||
id: string;
|
||||
last_name: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
interface DatasetListProps {
|
||||
addDangerToast: (msg: string) => void;
|
||||
addSuccessToast: (msg: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
datasets: any[];
|
||||
datasetCount: number;
|
||||
loading: boolean;
|
||||
filterOperators: FilterOperatorMap;
|
||||
filters: Filters;
|
||||
owners: Array<{ text: string; value: number }>;
|
||||
databases: Array<{ text: string; value: number }>;
|
||||
permissions: string[];
|
||||
lastFetchDataConfig: FetchDataConfig | null;
|
||||
}
|
||||
|
||||
interface Dataset {
|
||||
changed_by: string;
|
||||
changed_by_name: string;
|
||||
|
@ -77,24 +70,78 @@ interface Dataset {
|
|||
table_name: string;
|
||||
}
|
||||
|
||||
class DatasetList extends React.PureComponent<Props, State> {
|
||||
static propTypes = {
|
||||
addDangerToast: PropTypes.func.isRequired,
|
||||
const DatasetList: FunctionComponent<DatasetListProps> = ({
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
}) => {
|
||||
const [databases, setDatabases] = useState<{ text: string; value: number }[]>(
|
||||
[],
|
||||
);
|
||||
const [datasetCount, setDatasetCount] = useState(0);
|
||||
const [datasetCurrentlyDeleting, setDatasetCurrentlyDeleting] = useState<
|
||||
(Dataset & { chart_count: number; dashboard_count: number }) | null
|
||||
>(null);
|
||||
const [datasets, setDatasets] = useState<any[]>([]);
|
||||
const [currentFilters, setCurrentFilters] = useState<Filters>([]);
|
||||
const [
|
||||
lastFetchDataConfig,
|
||||
setLastFetchDataConfig,
|
||||
] = useState<FetchDataConfig | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentOwners, setCurrentOwners] = useState<
|
||||
{ text: string; value: number }[]
|
||||
>([]);
|
||||
const [permissions, setPermissions] = useState<string[]>([]);
|
||||
|
||||
const updateFilters = (filterOperators: FilterOperatorMap) => {
|
||||
const convertFilter = ({
|
||||
name: label,
|
||||
operator,
|
||||
}: {
|
||||
name: string;
|
||||
operator: string;
|
||||
}) => ({ label, value: operator });
|
||||
setCurrentFilters([
|
||||
{
|
||||
Header: 'Database',
|
||||
id: 'database',
|
||||
input: 'select',
|
||||
operators: filterOperators.database.map(convertFilter),
|
||||
selects: databases.map(({ text: label, value }) => ({
|
||||
label,
|
||||
value,
|
||||
})),
|
||||
},
|
||||
{
|
||||
Header: 'Schema',
|
||||
id: 'schema',
|
||||
operators: filterOperators.schema.map(convertFilter),
|
||||
},
|
||||
{
|
||||
Header: 'Table Name',
|
||||
id: 'table_name',
|
||||
operators: filterOperators.table_name.map(convertFilter),
|
||||
},
|
||||
{
|
||||
Header: 'Owners',
|
||||
id: 'owners',
|
||||
input: 'select',
|
||||
operators: filterOperators.owners.map(convertFilter),
|
||||
selects: currentOwners.map(({ text: label, value }) => ({
|
||||
label,
|
||||
value,
|
||||
})),
|
||||
},
|
||||
{
|
||||
Header: 'SQL Lab View',
|
||||
id: 'is_sqllab_view',
|
||||
input: 'checkbox',
|
||||
operators: filterOperators.is_sqllab_view.map(convertFilter),
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
state: State = {
|
||||
datasetCount: 0,
|
||||
datasets: [],
|
||||
filterOperators: {},
|
||||
filters: [],
|
||||
lastFetchDataConfig: null,
|
||||
loading: true,
|
||||
owners: [],
|
||||
databases: [],
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const fetchDataset = () => {
|
||||
Promise.all([
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/dataset/_info`,
|
||||
|
@ -111,20 +158,13 @@ class DatasetList extends React.PureComponent<Props, State> {
|
|||
{ json: ownersJson = {} },
|
||||
{ json: databasesJson = {} },
|
||||
]) => {
|
||||
this.setState(
|
||||
{
|
||||
filterOperators: infoJson.filters,
|
||||
owners: ownersJson.result,
|
||||
databases: databasesJson.result,
|
||||
permissions: infoJson.permissions,
|
||||
},
|
||||
this.updateFilters,
|
||||
);
|
||||
setCurrentOwners(ownersJson.result);
|
||||
setDatabases(databasesJson.result);
|
||||
setPermissions(infoJson.permissions);
|
||||
updateFilters(infoJson.filters);
|
||||
},
|
||||
([e1, e2]) => {
|
||||
this.props.addDangerToast(
|
||||
t('An error occurred while fetching datasets'),
|
||||
);
|
||||
addDangerToast(t('An error occurred while fetching datasets'));
|
||||
if (e1) {
|
||||
console.error(e1);
|
||||
}
|
||||
|
@ -133,23 +173,48 @@ class DatasetList extends React.PureComponent<Props, State> {
|
|||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
get canEdit() {
|
||||
return this.hasPerm('can_edit');
|
||||
}
|
||||
useEffect(() => {
|
||||
fetchDataset();
|
||||
}, []);
|
||||
|
||||
get canDelete() {
|
||||
return this.hasPerm('can_delete');
|
||||
}
|
||||
const hasPerm = (perm: string) => {
|
||||
if (!permissions.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
get canCreate() {
|
||||
return this.hasPerm('can_add');
|
||||
}
|
||||
return Boolean(permissions.find(p => p === perm));
|
||||
};
|
||||
|
||||
initialSort = [{ id: 'changed_on', desc: true }];
|
||||
const canEdit = () => hasPerm('can_edit');
|
||||
const canDelete = () => hasPerm('can_delete');
|
||||
const canCreate = () => hasPerm('can_add');
|
||||
|
||||
columns = [
|
||||
const initialSort = [{ id: 'changed_on', desc: true }];
|
||||
|
||||
const handleDatasetEdit = ({ id }: { id: number }) => {
|
||||
window.location.assign(`/tablemodelview/edit/${id}`);
|
||||
};
|
||||
|
||||
const openDatasetDeleteModal = (dataset: Dataset) =>
|
||||
SupersetClient.get({
|
||||
endpoint: `/api/v1/dataset/${dataset.id}/related_objects`,
|
||||
})
|
||||
.then(({ json = {} }) => {
|
||||
setDatasetCurrentlyDeleting({
|
||||
...dataset,
|
||||
chart_count: json.charts.count,
|
||||
dashboard_count: json.dashboards.count,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
addDangerToast(
|
||||
t('An error occurred while fetching dataset related data'),
|
||||
);
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
Cell: ({
|
||||
row: {
|
||||
|
@ -285,9 +350,9 @@ class DatasetList extends React.PureComponent<Props, State> {
|
|||
},
|
||||
{
|
||||
Cell: ({ row: { state, original } }: any) => {
|
||||
const handleDelete = () => this.handleDatasetDelete(original);
|
||||
const handleEdit = () => this.handleDatasetEdit(original);
|
||||
if (!this.canEdit && !this.canDelete) {
|
||||
const handleEdit = () => handleDatasetEdit(original);
|
||||
const handleDelete = () => openDatasetDeleteModal(original);
|
||||
if (!canEdit() && !canDelete()) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
|
@ -308,36 +373,24 @@ class DatasetList extends React.PureComponent<Props, State> {
|
|||
<Icon name="compass" />
|
||||
</a>
|
||||
</TooltipWrapper>
|
||||
{this.canDelete && (
|
||||
<ConfirmStatusChange
|
||||
title={t('Please Confirm')}
|
||||
description={
|
||||
<>
|
||||
{t('Are you sure you want to delete ')}{' '}
|
||||
<b>{original.table_name}</b>?
|
||||
</>
|
||||
}
|
||||
onConfirm={handleDelete}
|
||||
{canDelete && (
|
||||
<TooltipWrapper
|
||||
label="delete-action"
|
||||
tooltip={t('Delete')}
|
||||
placement="bottom"
|
||||
>
|
||||
{confirmDelete => (
|
||||
<TooltipWrapper
|
||||
label="delete-action"
|
||||
tooltip={t('Delete')}
|
||||
placement="bottom"
|
||||
>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={confirmDelete}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
</ConfirmStatusChange>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</span>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
{this.canEdit && (
|
||||
|
||||
{canEdit() && (
|
||||
<TooltipWrapper
|
||||
label="edit-action"
|
||||
tooltip={t('Edit')}
|
||||
|
@ -362,7 +415,7 @@ class DatasetList extends React.PureComponent<Props, State> {
|
|||
},
|
||||
];
|
||||
|
||||
menu = {
|
||||
const menu = {
|
||||
name: t('Data'),
|
||||
createButton: {
|
||||
name: t('Dataset'),
|
||||
|
@ -383,194 +436,147 @@ class DatasetList extends React.PureComponent<Props, State> {
|
|||
],
|
||||
};
|
||||
|
||||
hasPerm = (perm: string) => {
|
||||
if (!this.state.permissions.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(this.state.permissions.find(p => p === perm));
|
||||
const closeDatasetDeleteModal = () => {
|
||||
setDatasetCurrentlyDeleting(null);
|
||||
};
|
||||
|
||||
handleDatasetEdit = ({ id }: { id: number }) => {
|
||||
window.location.assign(`/tablemodelview/edit/${id}`);
|
||||
};
|
||||
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,
|
||||
}));
|
||||
|
||||
handleDatasetDelete = ({ id, table_name: tableName }: Dataset) =>
|
||||
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);
|
||||
})
|
||||
.catch(() => {
|
||||
addDangerToast(t('An error occurred while fetching datasets'));
|
||||
setLoading(false);
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const handleDatasetDelete = ({ id, table_name: tableName }: Dataset) => {
|
||||
SupersetClient.delete({
|
||||
endpoint: `/api/v1/dataset/${id}`,
|
||||
}).then(
|
||||
() => {
|
||||
const { lastFetchDataConfig } = this.state;
|
||||
if (lastFetchDataConfig) {
|
||||
this.fetchData(lastFetchDataConfig);
|
||||
fetchData(lastFetchDataConfig);
|
||||
}
|
||||
this.props.addSuccessToast(t('Deleted: %s', tableName));
|
||||
setDatasetCurrentlyDeleting(null);
|
||||
addSuccessToast(t('Deleted: %s', tableName));
|
||||
},
|
||||
(err: any) => {
|
||||
console.error(err);
|
||||
this.props.addDangerToast(
|
||||
t('There was an issue deleting %s', tableName),
|
||||
);
|
||||
addDangerToast(t('There was an issue deleting %s', tableName));
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
handleBulkDatasetDelete = (datasets: Dataset[]) => {
|
||||
const handleBulkDatasetDelete = () => {
|
||||
SupersetClient.delete({
|
||||
endpoint: `/api/v1/dataset/?q=${rison.encode(
|
||||
datasets.map(({ id }) => id),
|
||||
)}`,
|
||||
}).then(
|
||||
({ json = {} }) => {
|
||||
const { lastFetchDataConfig } = this.state;
|
||||
if (lastFetchDataConfig) {
|
||||
this.fetchData(lastFetchDataConfig);
|
||||
fetchData(lastFetchDataConfig);
|
||||
}
|
||||
this.props.addSuccessToast(json.message);
|
||||
addSuccessToast(json.message);
|
||||
},
|
||||
(err: any) => {
|
||||
console.error(err);
|
||||
this.props.addDangerToast(
|
||||
t('There was an issue deleting the selected datasets'),
|
||||
);
|
||||
addDangerToast(t('There was an issue deleting the selected datasets'));
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
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/dataset/?q=${queryParams}`,
|
||||
})
|
||||
.then(({ json = {} }) => {
|
||||
this.setState({ datasets: json.result, datasetCount: json.count });
|
||||
})
|
||||
.catch(() => {
|
||||
this.props.addDangerToast(
|
||||
t('An error occurred while fetching datasets'),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
};
|
||||
|
||||
updateFilters = () => {
|
||||
const { filterOperators, owners, databases } = this.state;
|
||||
const convertFilter = ({
|
||||
name: label,
|
||||
operator,
|
||||
}: {
|
||||
name: string;
|
||||
operator: string;
|
||||
}) => ({ label, value: operator });
|
||||
|
||||
this.setState({
|
||||
filters: [
|
||||
{
|
||||
Header: 'Database',
|
||||
id: 'database',
|
||||
input: 'select',
|
||||
operators: filterOperators.database.map(convertFilter),
|
||||
selects: databases.map(({ text: label, value }) => ({
|
||||
label,
|
||||
value,
|
||||
})),
|
||||
},
|
||||
{
|
||||
Header: 'Schema',
|
||||
id: 'schema',
|
||||
operators: filterOperators.schema.map(convertFilter),
|
||||
},
|
||||
{
|
||||
Header: 'Table Name',
|
||||
id: 'table_name',
|
||||
operators: filterOperators.table_name.map(convertFilter),
|
||||
},
|
||||
{
|
||||
Header: 'Owners',
|
||||
id: 'owners',
|
||||
input: 'select',
|
||||
operators: filterOperators.owners.map(convertFilter),
|
||||
selects: owners.map(({ text: label, value }) => ({ label, value })),
|
||||
},
|
||||
{
|
||||
Header: 'SQL Lab View',
|
||||
id: 'is_sqllab_view',
|
||||
input: 'checkbox',
|
||||
operators: filterOperators.is_sqllab_view.map(convertFilter),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { datasets, datasetCount, loading, filters } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubMenu {...this.menu} canCreate={this.canCreate} />
|
||||
<ConfirmStatusChange
|
||||
title={t('Please confirm')}
|
||||
description={t(
|
||||
'Are you sure you want to delete the selected datasets?',
|
||||
)}
|
||||
onConfirm={this.handleBulkDatasetDelete}
|
||||
>
|
||||
{confirmDelete => {
|
||||
const bulkActions = [];
|
||||
if (this.canDelete) {
|
||||
bulkActions.push({
|
||||
key: 'delete',
|
||||
name: (
|
||||
<>
|
||||
<i className="fa fa-trash" /> {t('Delete')}
|
||||
</>
|
||||
),
|
||||
onSelect: confirmDelete,
|
||||
});
|
||||
}
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<SubMenu {...menu} canCreate={canCreate()} />
|
||||
<ConfirmStatusChange
|
||||
title={t('Please confirm')}
|
||||
description={t(
|
||||
'Are you sure you want to delete the selected datasets?',
|
||||
)}
|
||||
onConfirm={handleBulkDatasetDelete}
|
||||
>
|
||||
{confirmDelete => {
|
||||
const bulkActions = [];
|
||||
if (canDelete()) {
|
||||
bulkActions.push({
|
||||
key: 'delete',
|
||||
name: (
|
||||
<>
|
||||
<i className="fa fa-trash" /> {t('Delete')}
|
||||
</>
|
||||
),
|
||||
onSelect: confirmDelete,
|
||||
});
|
||||
}
|
||||
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?')}
|
||||
/>
|
||||
)}
|
||||
<ListView
|
||||
className="dataset-list-view"
|
||||
columns={this.columns}
|
||||
columns={columns}
|
||||
data={datasets}
|
||||
count={datasetCount}
|
||||
pageSize={PAGE_SIZE}
|
||||
fetchData={this.fetchData}
|
||||
fetchData={fetchData}
|
||||
loading={loading}
|
||||
initialSort={this.initialSort}
|
||||
filters={filters}
|
||||
initialSort={initialSort}
|
||||
filters={currentFilters}
|
||||
bulkActions={bulkActions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ConfirmStatusChange>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ConfirmStatusChange>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withToasts(DatasetList);
|
||||
|
|
|
@ -16,14 +16,14 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import styled from '@superset-ui/style';
|
||||
import { SupersetClient } from '@superset-ui/connection';
|
||||
import { t } from '@superset-ui/translation';
|
||||
import { isEmpty, isNil } from 'lodash';
|
||||
import Icon from 'src/components/Icon';
|
||||
import TableSelector from 'src/components/TableSelector';
|
||||
import Modal from './Modal';
|
||||
import Modal from 'src/components/Modal';
|
||||
import withToasts from '../../messageToasts/enhancers/withToasts';
|
||||
|
||||
interface DatasetModalProps {
|
||||
|
@ -33,35 +33,29 @@ interface DatasetModalProps {
|
|||
show: boolean;
|
||||
}
|
||||
|
||||
interface DatasetModalState {
|
||||
datasourceId?: number;
|
||||
disableSave: boolean;
|
||||
schema: string;
|
||||
tableName: string;
|
||||
}
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
margin: auto 10px auto 0;
|
||||
`;
|
||||
|
||||
class DatasetModal extends React.PureComponent<
|
||||
DatasetModalProps,
|
||||
DatasetModalState
|
||||
> {
|
||||
constructor(props: DatasetModalProps) {
|
||||
super(props);
|
||||
this.onSave = this.onSave.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
const TableSelectorContainer = styled.div`
|
||||
.TableSelector {
|
||||
padding-bottom: 340px;
|
||||
width: 65%;
|
||||
}
|
||||
`;
|
||||
|
||||
state: DatasetModalState = {
|
||||
datasourceId: undefined,
|
||||
disableSave: true,
|
||||
schema: '',
|
||||
tableName: '',
|
||||
};
|
||||
const DatasetModal: FunctionComponent<DatasetModalProps> = ({
|
||||
addDangerToast,
|
||||
addSuccessToast,
|
||||
onHide,
|
||||
show,
|
||||
}) => {
|
||||
const [datasourceId, setDatasourceId] = useState<number | null>(null);
|
||||
const [disableSave, setDisableSave] = useState(true);
|
||||
const [currentSchema, setSchema] = useState('');
|
||||
const [currentTableName, setTableName] = useState('');
|
||||
|
||||
onChange({
|
||||
const onChange = ({
|
||||
dbId,
|
||||
schema,
|
||||
tableName,
|
||||
|
@ -69,61 +63,62 @@ class DatasetModal extends React.PureComponent<
|
|||
dbId: number;
|
||||
schema: string;
|
||||
tableName: string;
|
||||
}) {
|
||||
const disableSave = isNil(dbId) || isEmpty(schema) || isEmpty(tableName);
|
||||
this.setState({
|
||||
datasourceId: dbId,
|
||||
disableSave,
|
||||
schema,
|
||||
tableName,
|
||||
});
|
||||
}
|
||||
}) => {
|
||||
setDatasourceId(dbId);
|
||||
setDisableSave(isNil(dbId) || isEmpty(schema) || isEmpty(tableName));
|
||||
setSchema(schema);
|
||||
setTableName(tableName);
|
||||
};
|
||||
|
||||
onSave() {
|
||||
const { datasourceId, schema, tableName } = this.state;
|
||||
const data = { database: datasourceId, schema, table_name: tableName };
|
||||
const onSave = () => {
|
||||
const data = {
|
||||
database: datasourceId,
|
||||
schema: currentSchema,
|
||||
table_name: currentTableName,
|
||||
};
|
||||
SupersetClient.post({
|
||||
endpoint: '/api/v1/dataset/',
|
||||
body: JSON.stringify(data),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.then(() => {
|
||||
this.props.addSuccessToast(t('The dataset has been saved'));
|
||||
this.props.onHide();
|
||||
addSuccessToast(t('The dataset has been saved'));
|
||||
onHide();
|
||||
})
|
||||
.catch(e => {
|
||||
this.props.addDangerToast(t('Error while saving dataset'));
|
||||
addDangerToast(t('Error while saving dataset'));
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
disableSave={this.state.disableSave}
|
||||
onHide={this.props.onHide}
|
||||
onSave={this.onSave}
|
||||
show={this.props.show}
|
||||
title={
|
||||
<>
|
||||
<StyledIcon name="warning" />
|
||||
{t('Add Dataset')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
return (
|
||||
<Modal
|
||||
disablePrimaryButton={disableSave}
|
||||
onHandledPrimaryAction={onSave}
|
||||
onHide={onHide}
|
||||
primaryButtonName={t('Add')}
|
||||
show={show}
|
||||
title={
|
||||
<>
|
||||
<StyledIcon name="warning" />
|
||||
{t('Add Dataset')}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TableSelectorContainer>
|
||||
<TableSelector
|
||||
clearable={false}
|
||||
dbId={this.state.datasourceId}
|
||||
dbId={datasourceId}
|
||||
formMode
|
||||
handleError={this.props.addDangerToast}
|
||||
onChange={this.onChange}
|
||||
schema={this.state.schema}
|
||||
handleError={addDangerToast}
|
||||
onChange={onChange}
|
||||
schema={currentSchema}
|
||||
sqlLabMode={false}
|
||||
tableName={this.state.tableName}
|
||||
tableName={currentTableName}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
</TableSelectorContainer>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default withToasts(DatasetModal);
|
||||
|
|
Loading…
Reference in New Issue