Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8bdd2df
working updated list
hughhhh Dec 2, 2020
f928411
cleanup
hughhhh Dec 2, 2020
ca6b16e
remove console.logs
hughhhh Dec 2, 2020
49ff839
change uid
hughhhh Dec 2, 2020
70d4004
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Dec 3, 2020
388737a
update
hughhhh Dec 7, 2020
c542f94
remove old test
hughhhh Dec 7, 2020
f37293a
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Dec 8, 2020
36d7782
fix linting
hughhhh Dec 8, 2020
ef7a8d9
add confirm overwrite modal
hughhhh Dec 8, 2020
85bdbef
fixed the test
hughhhh Dec 8, 2020
76bca6c
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Dec 9, 2020
5592a91
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Dec 9, 2020
b30a337
refactor component to have search api
hughhhh Dec 9, 2020
6395dbe
pulled refactored PR
hughhhh Dec 9, 2020
ef49578
add success toast
hughhhh Dec 9, 2020
7d3507e
fix linting
hughhhh Dec 9, 2020
da2c09b
additional step for testing
hughhhh Dec 9, 2020
3f839c7
refactor for hooks
hughhhh Dec 10, 2020
40b82ac
switched out for hooks
hughhhh Dec 10, 2020
9535d57
update search calls
hughhhh Dec 10, 2020
011ada8
cleaning things up
hughhhh Dec 10, 2020
89d9381
updated component
hughhhh Dec 10, 2020
e04c792
change messaging
hughhhh Dec 10, 2020
16e37d9
use debouncing on network calls
hughhhh Dec 11, 2020
c0f8da0
fix test
hughhhh Dec 11, 2020
e296166
fix merge conflicts
hughhhh Dec 11, 2020
0c95807
fix more errors
hughhhh Dec 11, 2020
027b422
add debonuncing effect to utils fils
hughhhh Dec 13, 2020
28f3c47
address comments
hughhhh Dec 14, 2020
f71cf92
update message
hughhhh Dec 14, 2020
53cd8e7
Merge branch 'master' of https://github.com/apache/incubator-superset…
hughhhh Dec 14, 2020
76d91f8
fix linting issue
hughhhh Dec 14, 2020
ffd5226
fix linting issues
hughhhh Dec 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import thunk from 'redux-thunk';
import { act } from 'react-dom/test-utils';
import sinon from 'sinon';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import { act } from 'react-dom/test-utils';
import Modal from 'src/common/components/Modal';
import ChangeDatasourceModal from 'src/datasource/ChangeDatasourceModal';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
Expand All @@ -47,11 +47,12 @@ const datasourceData = {
uid: datasource.id,
};

const DATASOURCES_ENDPOINT = 'glob:*/superset/datasources/';
const DATASOURCES_ENDPOINT =
'glob:*/api/v1/dataset/?q=(order_column:changed_on_delta_humanized,order_direction:asc,page:0,page_size:20)';
const DATASOURCE_ENDPOINT = `glob:*/datasource/get/${datasourceData.type}/${datasourceData.id}`;
const DATASOURCE_PAYLOAD = { new: 'data' };

fetchMock.get(DATASOURCES_ENDPOINT, [mockDatasource['7__table']]);
fetchMock.get(DATASOURCES_ENDPOINT, { result: [mockDatasource['7__table']] });
fetchMock.get(DATASOURCE_ENDPOINT, DATASOURCE_PAYLOAD);

async function mountAndWait(props = mockedProps) {
Expand Down Expand Up @@ -80,14 +81,29 @@ describe('ChangeDatasourceModal', () => {
});

it('fetches datasources', async () => {
expect(fetchMock.calls(/superset\/datasources/)).toHaveLength(3);
expect(fetchMock.calls(/api\/v1\/dataset/)).toHaveLength(6);
});

it('renders confirmation message', async () => {
act(() => {
wrapper.find('.datasource-link').at(0).props().onClick();
});
await waitForComponentToPaint(wrapper);

expect(wrapper.find('.proceed-btn')).toExist();
});

it('changes the datasource', async () => {
act(() => {
wrapper.find('.datasource-link').at(0).props().onClick(datasourceData);
wrapper.find('.datasource-link').at(0).props().onClick();
});
await waitForComponentToPaint(wrapper);

act(() => {
wrapper.find('.proceed-btn').at(0).props().onClick(datasourceData);
});
await waitForComponentToPaint(wrapper);

expect(fetchMock.calls(/datasource\/get\/table\/7/)).toHaveLength(1);
});
});
268 changes: 173 additions & 95 deletions superset-frontend/src/datasource/ChangeDatasourceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,53 @@ import React, {
FunctionComponent,
useState,
useRef,
useMemo,
useEffect,
useCallback,
} from 'react';
import { Alert, FormControl, FormControlProps } from 'react-bootstrap';
import { SupersetClient, t } from '@superset-ui/core';
import { SupersetClient, t, styled } from '@superset-ui/core';
import TableView from 'src/components/TableView';
import Modal from 'src/common/components/Modal';
import StyledModal from 'src/common/components/Modal';
import Button from 'src/components/Button';
import { useListViewResource } from 'src/views/CRUD/hooks';
import Dataset from 'src/types/Dataset';
import { useDebouncedEffect } from 'src/explore/exploreUtils';
import { getClientErrorObject } from '../utils/getClientErrorObject';
import Loading from '../components/Loading';
import withToasts from '../messageToasts/enhancers/withToasts';

const CONFIRM_WARNING_MESSAGE = t(
'Warning! Changing the dataset may break the chart if the metadata (columns/metrics) does not exist in the target dataset',
);

interface Datasource {
type: string;
id: number;
uid: string;
}

interface ChangeDatasourceModalProps {
addDangerToast: (msg: string) => void;
onChange: (id: number) => void;
addSuccessToast: (msg: string) => void;
onChange: (uid: string) => void;
onDatasourceSave: (datasource: object, errors?: Array<any>) => {};
onHide: () => void;
show: boolean;
}

const ConfirmModalStyled = styled.div`
.btn-container {
display: flex;
justify-content: flex-end;
padding: 0px 15px;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be able to use some of the theme gridunits here

margin: 10px 0 0 0;
}

.confirm-modal-container {
margin: 9px;
}
`;

const TABLE_COLUMNS = [
'name',
'type',
Expand All @@ -47,86 +75,78 @@ const TABLE_COLUMNS = [
'creator',
].map(col => ({ accessor: col, Header: col }));

const TABLE_FILTERABLE = ['rawName', 'type', 'schema', 'connection', 'creator'];
const CHANGE_WARNING_MSG = t(
'Changing the dataset may break the chart if the chart relies ' +
'on columns or metadata that does not exist in the target dataset',
);

const emptyRequest = {
pageIndex: 0,
pageSize: 20,
filters: [],
sortBy: [{ id: 'changed_on_delta_humanized' }],
};

const ChangeDatasourceModal: FunctionComponent<ChangeDatasourceModalProps> = ({
addDangerToast,
addSuccessToast,
onChange,
onDatasourceSave,
onHide,
show,
}) => {
const [datasources, setDatasources] = useState<any>(null);
const [filter, setFilter] = useState<any>(undefined);
const [loading, setLoading] = useState(true);
const [confirmChange, setConfirmChange] = useState(false);
const [confirmedDataset, setConfirmedDataset] = useState<Datasource>();
let searchRef = useRef<HTMLInputElement>(null);

useEffect(() => {
const selectDatasource = (datasource: any) => {
SupersetClient.get({
endpoint: `/datasource/get/${datasource.type}/${datasource.id}`,
})
.then(({ json }) => {
onDatasourceSave(json);
onChange(datasource.uid);
})
.catch(response => {
getClientErrorObject(response).then(
({ error, message }: { error: any; message: string }) => {
const errorMessage = error
? error.error || error.statusText || error
: message;
addDangerToast(errorMessage);
},
);
});
onHide();
};
const {
state: { loading, resourceCollection },
fetchData,
} = useListViewResource<Dataset>('dataset', t('dataset'), addDangerToast);

const selectDatasource = useCallback((datasource: Datasource) => {
setConfirmChange(true);
setConfirmedDataset(datasource);
}, []);

useDebouncedEffect(() => {
if (filter) {
fetchData({
...emptyRequest,
filters: [
{
id: 'table_name',
operator: 'ct',
value: filter,
},
],
});
}
}, 1000);

const onEnterModal = () => {
useEffect(() => {
const onEnterModal = async () => {
if (searchRef && searchRef.current) {
searchRef.current.focus();
}
if (!datasources) {
SupersetClient.get({
endpoint: '/superset/datasources/',
})
.then(({ json }) => {
const data = json.map((ds: any) => ({
rawName: ds.name,
connection: ds.connection,
schema: ds.schema,
name: (
<a
href="#"
onClick={() => selectDatasource(ds)}
className="datasource-link"
>
{ds.name}
</a>
),
type: ds.type,
}));
setLoading(false);
setDatasources(data);
})
.catch(response => {
setLoading(false);
getClientErrorObject(response).then(({ error }: any) => {
addDangerToast(error.error || error.statusText || error);
});
});
}

// Fetch initial datasets for tableview
await fetchData(emptyRequest);
};

if (show) {
onEnterModal();
}
}, [addDangerToast, datasources, onChange, onDatasourceSave, onHide, show]);
}, [
addDangerToast,
fetchData,
onChange,
onDatasourceSave,
onHide,
selectDatasource,
show,
]);

const setSearchRef = (ref: any) => {
searchRef = ref;
Expand All @@ -135,54 +155,112 @@ const ChangeDatasourceModal: FunctionComponent<ChangeDatasourceModalProps> = ({
const changeSearch = (
event: React.FormEvent<FormControl & FormControlProps>,
) => {
setFilter((event.currentTarget?.value as string) ?? '');
const searchValue = (event.currentTarget?.value as string) ?? '';
setFilter(searchValue);
};

const data = useMemo(
() =>
filter && datasources
? datasources.filter((datasource: any) =>
TABLE_FILTERABLE.some(field => datasource[field]?.includes(filter)),
)
: datasources,
[datasources, filter],
);
const handleChangeConfirm = () => {
SupersetClient.get({
endpoint: `/datasource/get/${confirmedDataset?.type}/${confirmedDataset?.id}`,
})
.then(({ json }) => {
onDatasourceSave(json);
onChange(`${confirmedDataset?.id}__table`);
})
.catch(response => {
getClientErrorObject(response).then(
({ error, message }: { error: any; message: string }) => {
const errorMessage = error
? error.error || error.statusText || error
: message;
addDangerToast(errorMessage);
},
);
});
onHide();
addSuccessToast('Successfully changed datasource!');
};

const handlerCancelConfirm = () => {
setConfirmChange(false);
};

const renderTableView = () => {
const data = resourceCollection.map((ds: any) => ({
rawName: ds.table_name,
connection: ds.database.database_name,
schema: ds.schema,
name: (
<a
href="#"
onClick={() => selectDatasource({ type: 'table', ...ds })}
className="datasource-link"
>
{ds.table_name}
</a>
),
type: ds.kind,
}));

return data;
};

return (
<Modal
<StyledModal
show={show}
onHide={onHide}
responsive
title={t('Select a dataset')}
hideFooter
>
<>
<Alert bsStyle="warning">
<strong>{t('Warning!')}</strong> {CHANGE_WARNING_MSG}
</Alert>
<div>
<FormControl
inputRef={ref => {
setSearchRef(ref);
}}
type="text"
bsSize="sm"
value={filter}
placeholder={t('Search / Filter')}
onChange={changeSearch}
/>
</div>
{loading && <Loading />}
{datasources && (
<TableView
columns={TABLE_COLUMNS}
data={data}
pageSize={20}
className="table-condensed"
/>
{!confirmChange && (
<>
<Alert bsStyle="warning">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the warning message here, I should use it in the overwrite confirmation dialog!

<strong>{t('Warning!')}</strong> {CHANGE_WARNING_MSG}
</Alert>
<div>
<FormControl
inputRef={ref => {
setSearchRef(ref);
}}
type="text"
bsSize="sm"
value={filter}
placeholder={t('Search / Filter')}
onChange={changeSearch}
/>
</div>
{loading && <Loading />}
{!loading && (
<TableView
columns={TABLE_COLUMNS}
data={renderTableView()}
pageSize={20}
className="table-condensed"
/>
)}
</>
)}
{confirmChange && (
<ConfirmModalStyled>
<div className="confirm-modal-container">
{CONFIRM_WARNING_MESSAGE}
<div className="btn-container">
<Button onClick={handlerCancelConfirm}>Cancel</Button>
<Button
className="proceed-btn"
buttonStyle="primary"
onClick={handleChangeConfirm}
>
Proceed
</Button>
</div>
</div>
</ConfirmModalStyled>
)}
</>
</Modal>
</StyledModal>
);
};

Expand Down
Loading