mirror of
https://github.com/apache/superset.git
synced 2026-04-27 03:55:47 +00:00
feat(explore): Frontend implementation of dataset creation from infobox (#19855)
* Frontend implementation of create dataset from infobox * Fixed sl_dataset type * Fix test * Fixed sl_dataset type (forgot to save) * RTL testing * Adjusted styling/text on infobox and save dataset modal * Appease lint * Make infobox invisible and fix tests * Remove unnecessary placeholder * Move types to sql lab * Moved logic into save dataset modal * Change DatasourceMeta type to Dataset * Add ExploreDatasource union type to save dataset modal * Get user info from redux inside save dataset modal * Addressed comments * Adjusting to new query type * Fixed save dataset in explore and union type * Added testing * Defined d for queries * Remove dataset from SaveDatasetModal * Clarify useSelector parameter * Fix dndControls union type * Fix shared-controls union type * Fix controlPanel union type * Move ExploreRootState to explore type file * Remove unnecessary testing playground * Move datasource type check in DatasourcePanel to a function * Make all sqllab Query imports reference @superset-ui/core Query type * Deconstruct query props in ResultSet * Fix union type in /legacy-plugin-chart-heatmap/src/controlPanel * Change SaveDatasetModal tests to RTL * Cleaned datasourceTypeCheck * Fix infobox styling * Fix SaveDatasetModal test * Fix query fixture in sqllab and Query type in SaveDatasetModal test * Fix Query type and make test query fixture * Added columns to Query type, separated results property, created QueryResponse union type, and fixed all types affected * Fixed a couple missed broken types * Added ExploreDatasource to SqlLab type file * Removed unneeded Query import from DatasourcePanel * Address PR comments * Fix columnChoices * Fix all incorrect column property checks * Fix logic on dndGroupByControl * Dry up savedMetrics type check * Fixed TIME_COLUMN_OPTION * Dried savedMetrics type check even further * Change savedMetricsTypeCheck to defineSavedMetrics * Change datasourceTypeCheck to isValidDatasourceType * Fix Query path in groupByControl * dnd_granularity_sqla now sorts Query types with is_dttm at the top * Fixed/cleaned query sort * Add sortedQueryColumns and proper optional chaining to granularity_sqla * Move testQuery to core-ui, add test coverage for Queries in columnChoices * Moved DEFAULT_METRICS to core-ui and wrote a test for defineSavedMetrics * Add license and clean dataset test object * Change DatasourceType.Dataset to dataset
This commit is contained in:
committed by
GitHub
parent
d1c24f81f2
commit
ba0c37d3df
@@ -22,7 +22,7 @@ import { t } from '@superset-ui/core';
|
||||
import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||
import Button from 'src/components/Button';
|
||||
import { exploreChart } from 'src/explore/exploreUtils';
|
||||
import { RootState } from 'src/SqlLab/types';
|
||||
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
|
||||
interface ExploreCtasResultsButtonProps {
|
||||
actions: {
|
||||
@@ -45,7 +45,7 @@ const ExploreCtasResultsButton = ({
|
||||
}: ExploreCtasResultsButtonProps) => {
|
||||
const { createCtasDatasource, addInfoToast, addDangerToast } = actions;
|
||||
const errorMessage = useSelector(
|
||||
(state: RootState) => state.sqlLab.errorMessage,
|
||||
(state: SqlLabRootState) => state.sqlLab.errorMessage,
|
||||
);
|
||||
|
||||
const buildVizOptions = {
|
||||
|
||||
@@ -18,12 +18,11 @@
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EmptyStateMedium } from 'src/components/EmptyState';
|
||||
import { t, styled } from '@superset-ui/core';
|
||||
import { Query } from 'src/SqlLab/types';
|
||||
import { t, styled, QueryResponse } from '@superset-ui/core';
|
||||
import QueryTable from 'src/SqlLab/components/QueryTable';
|
||||
|
||||
interface QueryHistoryProps {
|
||||
queries: Query[];
|
||||
queries: QueryResponse[];
|
||||
actions: {
|
||||
queryEditorSetAndSaveSql: Function;
|
||||
cloneQueryToNewTab: Function;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Button from 'src/components/Button';
|
||||
import Select from 'src/components/Select';
|
||||
import { styled, t, SupersetClient } from '@superset-ui/core';
|
||||
import { styled, t, SupersetClient, QueryResponse } from '@superset-ui/core';
|
||||
import { debounce } from 'lodash';
|
||||
import Loading from 'src/components/Loading';
|
||||
import {
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
epochTimeXYearsAgo,
|
||||
} from 'src/utils/dates';
|
||||
import AsyncSelect from 'src/components/AsyncSelect';
|
||||
import { Query } from 'src/SqlLab/types';
|
||||
import { STATUS_OPTIONS, TIME_OPTIONS } from 'src/SqlLab/constants';
|
||||
import QueryTable from '../QueryTable';
|
||||
|
||||
@@ -85,7 +84,7 @@ function QuerySearch({ actions, displayLimit }: QuerySearchProps) {
|
||||
const [from, setFrom] = useState<string>('28 days ago');
|
||||
const [to, setTo] = useState<string>('now');
|
||||
const [status, setStatus] = useState<string>('success');
|
||||
const [queriesArray, setQueriesArray] = useState<Query[]>([]);
|
||||
const [queriesArray, setQueriesArray] = useState<QueryResponse[]>([]);
|
||||
const [queriesLoading, setQueriesLoading] = useState<boolean>(true);
|
||||
|
||||
const getTimeFromSelection = (selection: string) => {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import React from 'react';
|
||||
import Label from 'src/components/Label';
|
||||
import { STATE_TYPE_MAP } from 'src/SqlLab/constants';
|
||||
import { Query } from 'src/SqlLab/types';
|
||||
import { Query } from '@superset-ui/core';
|
||||
|
||||
interface QueryStateLabelProps {
|
||||
query: Query;
|
||||
|
||||
@@ -21,14 +21,14 @@ import moment from 'moment';
|
||||
import Card from 'src/components/Card';
|
||||
import ProgressBar from 'src/components/ProgressBar';
|
||||
import Label from 'src/components/Label';
|
||||
import { t, useTheme } from '@superset-ui/core';
|
||||
import { t, useTheme, QueryResponse } from '@superset-ui/core';
|
||||
import { useSelector } from 'react-redux';
|
||||
import TableView from 'src/components/TableView';
|
||||
import Button from 'src/components/Button';
|
||||
import { fDuration } from 'src/utils/dates';
|
||||
import Icons from 'src/components/Icons';
|
||||
import { Tooltip } from 'src/components/Tooltip';
|
||||
import { Query, RootState } from 'src/SqlLab/types';
|
||||
import { SqlLabRootState } from 'src/SqlLab/types';
|
||||
import ModalTrigger from 'src/components/ModalTrigger';
|
||||
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
|
||||
import ResultSet from '../ResultSet';
|
||||
@@ -36,7 +36,7 @@ import HighlightedSql from '../HighlightedSql';
|
||||
import { StaticPosition, verticalAlign, StyledTooltip } from './styles';
|
||||
|
||||
interface QueryTableQuery
|
||||
extends Omit<Query, 'state' | 'sql' | 'progress' | 'results'> {
|
||||
extends Omit<QueryResponse, 'state' | 'sql' | 'progress' | 'results'> {
|
||||
state?: Record<string, any>;
|
||||
sql?: Record<string, any>;
|
||||
progress?: Record<string, any>;
|
||||
@@ -52,7 +52,7 @@ interface QueryTableProps {
|
||||
clearQueryResults: Function;
|
||||
removeQuery: Function;
|
||||
};
|
||||
queries?: Query[];
|
||||
queries?: QueryResponse[];
|
||||
onUserClicked?: Function;
|
||||
onDbClicked?: Function;
|
||||
displayLimit: number;
|
||||
@@ -91,7 +91,7 @@ const QueryTable = ({
|
||||
[columns],
|
||||
);
|
||||
|
||||
const user = useSelector<RootState, User>(state => state.sqlLab.user);
|
||||
const user = useSelector<SqlLabRootState, User>(state => state.sqlLab.user);
|
||||
|
||||
const {
|
||||
queryEditorSetAndSaveSql,
|
||||
@@ -102,15 +102,15 @@ const QueryTable = ({
|
||||
} = actions;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const restoreSql = (query: Query) => {
|
||||
const restoreSql = (query: QueryResponse) => {
|
||||
queryEditorSetAndSaveSql({ id: query.sqlEditorId }, query.sql);
|
||||
};
|
||||
|
||||
const openQueryInNewTab = (query: Query) => {
|
||||
const openQueryInNewTab = (query: QueryResponse) => {
|
||||
cloneQueryToNewTab(query, true);
|
||||
};
|
||||
|
||||
const openAsyncResults = (query: Query, displayLimit: number) => {
|
||||
const openAsyncResults = (query: QueryResponse, displayLimit: number) => {
|
||||
fetchQueryResults(query, displayLimit);
|
||||
};
|
||||
|
||||
|
||||
@@ -19,19 +19,9 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import ButtonGroup from 'src/components/ButtonGroup';
|
||||
import Alert from 'src/components/Alert';
|
||||
import moment from 'moment';
|
||||
import { RadioChangeEvent } from 'src/components';
|
||||
import Button from 'src/components/Button';
|
||||
import shortid from 'shortid';
|
||||
import rison from 'rison';
|
||||
import {
|
||||
styled,
|
||||
t,
|
||||
makeApi,
|
||||
SupersetClient,
|
||||
JsonResponse,
|
||||
} from '@superset-ui/core';
|
||||
import { debounce } from 'lodash';
|
||||
import { styled, t, QueryResponse } from '@superset-ui/core';
|
||||
import ErrorMessageWithStackTrace from 'src/components/ErrorMessage/ErrorMessageWithStackTrace';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
|
||||
@@ -42,26 +32,12 @@ import FilterableTable, {
|
||||
} from 'src/components/FilterableTable';
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||
import { prepareCopyToClipboardTabularData } from 'src/utils/common';
|
||||
import { exploreChart } from 'src/explore/exploreUtils';
|
||||
import { CtasEnum } from 'src/SqlLab/actions/sqlLab';
|
||||
import { Query } from 'src/SqlLab/types';
|
||||
import ExploreCtasResultsButton from '../ExploreCtasResultsButton';
|
||||
import ExploreResultsButton from '../ExploreResultsButton';
|
||||
import HighlightedSql from '../HighlightedSql';
|
||||
import QueryStateLabel from '../QueryStateLabel';
|
||||
|
||||
enum DatasetRadioState {
|
||||
SAVE_NEW = 1,
|
||||
OVERWRITE_DATASET = 2,
|
||||
}
|
||||
|
||||
const EXPLORE_CHART_DEFAULT = {
|
||||
metrics: [],
|
||||
groupby: [],
|
||||
time_range: 'No filter',
|
||||
viz_type: 'table',
|
||||
};
|
||||
|
||||
enum LIMITING_FACTOR {
|
||||
QUERY = 'QUERY',
|
||||
QUERY_AND_DROPDOWN = 'QUERY_AND_DROPDOWN',
|
||||
@@ -71,19 +47,6 @@ enum LIMITING_FACTOR {
|
||||
|
||||
const LOADING_STYLES: CSSProperties = { position: 'relative', minHeight: 100 };
|
||||
|
||||
interface DatasetOwner {
|
||||
first_name: string;
|
||||
id: number;
|
||||
last_name: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface DatasetOptionAutocomplete {
|
||||
value: string;
|
||||
datasetId: number;
|
||||
owners: [DatasetOwner];
|
||||
}
|
||||
|
||||
interface ResultSetProps {
|
||||
showControls?: boolean;
|
||||
actions: Record<string, any>;
|
||||
@@ -92,7 +55,7 @@ interface ResultSetProps {
|
||||
database?: Record<string, any>;
|
||||
displayLimit: number;
|
||||
height: number;
|
||||
query: Query;
|
||||
query: QueryResponse;
|
||||
search?: boolean;
|
||||
showSql?: boolean;
|
||||
visualize?: boolean;
|
||||
@@ -105,12 +68,6 @@ interface ResultSetState {
|
||||
showExploreResultsButton: boolean;
|
||||
data: Record<string, any>[];
|
||||
showSaveDatasetModal: boolean;
|
||||
newSaveDatasetName: string;
|
||||
saveDatasetRadioBtnState: number;
|
||||
shouldOverwriteDataSet: boolean;
|
||||
datasetToOverwrite: Record<string, any>;
|
||||
saveModalAutocompleteValue: string;
|
||||
userDatasetOptions: DatasetOptionAutocomplete[];
|
||||
alertIsOpen: boolean;
|
||||
}
|
||||
|
||||
@@ -145,44 +102,6 @@ const ResultSetErrorMessage = styled.div`
|
||||
padding-top: ${({ theme }) => 4 * theme.gridUnit}px;
|
||||
`;
|
||||
|
||||
const ResultSetRowsReturned = styled.span`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const LimitMessage = styled.span`
|
||||
color: ${({ theme }) => theme.colors.secondary.light1};
|
||||
margin-left: ${({ theme }) => theme.gridUnit * 2}px;
|
||||
`;
|
||||
|
||||
const updateDataset = async (
|
||||
dbId: number,
|
||||
datasetId: number,
|
||||
sql: string,
|
||||
columns: Array<Record<string, any>>,
|
||||
owners: [number],
|
||||
overrideColumns: boolean,
|
||||
) => {
|
||||
const endpoint = `api/v1/dataset/${datasetId}?override_columns=${overrideColumns}`;
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const body = JSON.stringify({
|
||||
sql,
|
||||
columns,
|
||||
owners,
|
||||
database_id: dbId,
|
||||
});
|
||||
|
||||
const data: JsonResponse = await SupersetClient.put({
|
||||
endpoint,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
return data.json.result;
|
||||
};
|
||||
|
||||
export default class ResultSet extends React.PureComponent<
|
||||
ResultSetProps,
|
||||
ResultSetState
|
||||
@@ -203,12 +122,6 @@ export default class ResultSet extends React.PureComponent<
|
||||
showExploreResultsButton: false,
|
||||
data: [],
|
||||
showSaveDatasetModal: false,
|
||||
newSaveDatasetName: this.getDefaultDatasetName(),
|
||||
saveDatasetRadioBtnState: DatasetRadioState.SAVE_NEW,
|
||||
shouldOverwriteDataSet: false,
|
||||
datasetToOverwrite: {},
|
||||
saveModalAutocompleteValue: '',
|
||||
userDatasetOptions: [],
|
||||
alertIsOpen: false,
|
||||
};
|
||||
this.changeSearch = this.changeSearch.bind(this);
|
||||
@@ -217,31 +130,11 @@ export default class ResultSet extends React.PureComponent<
|
||||
this.reFetchQueryResults = this.reFetchQueryResults.bind(this);
|
||||
this.toggleExploreResultsButton =
|
||||
this.toggleExploreResultsButton.bind(this);
|
||||
this.handleSaveInDataset = this.handleSaveInDataset.bind(this);
|
||||
this.handleHideSaveModal = this.handleHideSaveModal.bind(this);
|
||||
this.handleDatasetNameChange = this.handleDatasetNameChange.bind(this);
|
||||
this.handleSaveDatasetRadioBtnState =
|
||||
this.handleSaveDatasetRadioBtnState.bind(this);
|
||||
this.handleOverwriteCancel = this.handleOverwriteCancel.bind(this);
|
||||
this.handleOverwriteDataset = this.handleOverwriteDataset.bind(this);
|
||||
this.handleOverwriteDatasetOption =
|
||||
this.handleOverwriteDatasetOption.bind(this);
|
||||
this.handleSaveDatasetModalSearch = debounce(
|
||||
this.handleSaveDatasetModalSearch.bind(this),
|
||||
1000,
|
||||
);
|
||||
this.handleFilterAutocompleteOption =
|
||||
this.handleFilterAutocompleteOption.bind(this);
|
||||
this.handleOnChangeAutoComplete =
|
||||
this.handleOnChangeAutoComplete.bind(this);
|
||||
this.handleExploreBtnClick = this.handleExploreBtnClick.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
// only do this the first time the component is rendered/mounted
|
||||
this.reRunQueryIfSessionTimeoutErrorOnMount();
|
||||
const userDatasetsOwned = await this.getUserDatasets();
|
||||
this.setState({ userDatasetOptions: userDatasetsOwned });
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: ResultSetProps) {
|
||||
@@ -273,186 +166,7 @@ export default class ResultSet extends React.PureComponent<
|
||||
}
|
||||
};
|
||||
|
||||
getDefaultDatasetName = () =>
|
||||
`${this.props.query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`;
|
||||
|
||||
handleOnChangeAutoComplete = () => {
|
||||
this.setState({ datasetToOverwrite: {} });
|
||||
};
|
||||
|
||||
handleOverwriteDataset = async () => {
|
||||
const { sql, results, dbId } = this.props.query;
|
||||
const { datasetToOverwrite } = this.state;
|
||||
|
||||
await updateDataset(
|
||||
dbId,
|
||||
datasetToOverwrite.datasetId,
|
||||
sql,
|
||||
results.selected_columns.map(d => ({
|
||||
column_name: d.name,
|
||||
type: d.type,
|
||||
is_dttm: d.is_dttm,
|
||||
})),
|
||||
datasetToOverwrite.owners.map((o: DatasetOwner) => o.id),
|
||||
true,
|
||||
);
|
||||
|
||||
this.setState({
|
||||
showSaveDatasetModal: false,
|
||||
shouldOverwriteDataSet: false,
|
||||
datasetToOverwrite: {},
|
||||
newSaveDatasetName: this.getDefaultDatasetName(),
|
||||
});
|
||||
|
||||
exploreChart({
|
||||
...EXPLORE_CHART_DEFAULT,
|
||||
datasource: `${datasetToOverwrite.datasetId}__table`,
|
||||
all_columns: results.selected_columns.map(d => d.name),
|
||||
});
|
||||
};
|
||||
|
||||
handleSaveInDataset = () => {
|
||||
// if user wants to overwrite a dataset we need to prompt them
|
||||
if (
|
||||
this.state.saveDatasetRadioBtnState ===
|
||||
DatasetRadioState.OVERWRITE_DATASET
|
||||
) {
|
||||
this.setState({ shouldOverwriteDataSet: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const { schema, sql, dbId } = this.props.query;
|
||||
let { templateParams } = this.props.query;
|
||||
const selectedColumns = this.props.query?.results?.selected_columns || [];
|
||||
|
||||
// The filters param is only used to test jinja templates.
|
||||
// Remove the special filters entry from the templateParams
|
||||
// before saving the dataset.
|
||||
if (templateParams) {
|
||||
const p = JSON.parse(templateParams);
|
||||
/* eslint-disable-next-line no-underscore-dangle */
|
||||
if (p._filters) {
|
||||
/* eslint-disable-next-line no-underscore-dangle */
|
||||
delete p._filters;
|
||||
templateParams = JSON.stringify(p);
|
||||
}
|
||||
}
|
||||
|
||||
this.props.actions
|
||||
.createDatasource({
|
||||
schema,
|
||||
sql,
|
||||
dbId,
|
||||
templateParams,
|
||||
datasourceName: this.state.newSaveDatasetName,
|
||||
columns: selectedColumns,
|
||||
})
|
||||
.then((data: { table_id: number }) => {
|
||||
exploreChart({
|
||||
datasource: `${data.table_id}__table`,
|
||||
metrics: [],
|
||||
groupby: [],
|
||||
time_range: 'No filter',
|
||||
viz_type: 'table',
|
||||
all_columns: selectedColumns.map(c => c.name),
|
||||
row_limit: 1000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.props.actions.addDangerToast(
|
||||
t('An error occurred saving dataset'),
|
||||
);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
showSaveDatasetModal: false,
|
||||
newSaveDatasetName: this.getDefaultDatasetName(),
|
||||
});
|
||||
};
|
||||
|
||||
handleOverwriteDatasetOption = (
|
||||
_data: string,
|
||||
option: Record<string, any>,
|
||||
) => {
|
||||
this.setState({ datasetToOverwrite: option });
|
||||
};
|
||||
|
||||
handleDatasetNameChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
// @ts-expect-error
|
||||
this.setState({ newSaveDatasetName: e.target.value });
|
||||
};
|
||||
|
||||
handleHideSaveModal = () => {
|
||||
this.setState({
|
||||
showSaveDatasetModal: false,
|
||||
shouldOverwriteDataSet: false,
|
||||
});
|
||||
};
|
||||
|
||||
handleSaveDatasetRadioBtnState = (e: RadioChangeEvent) => {
|
||||
this.setState({ saveDatasetRadioBtnState: Number(e.target.value) });
|
||||
};
|
||||
|
||||
handleOverwriteCancel = () => {
|
||||
this.setState({ shouldOverwriteDataSet: false, datasetToOverwrite: {} });
|
||||
};
|
||||
|
||||
handleExploreBtnClick = () => {
|
||||
this.setState({
|
||||
showSaveDatasetModal: true,
|
||||
});
|
||||
};
|
||||
|
||||
getUserDatasets = async (searchText = '') => {
|
||||
// Making sure that autocomplete input has a value before rendering the dropdown
|
||||
// Transforming the userDatasetsOwned data for SaveModalComponent)
|
||||
const { userId } = this.props.user;
|
||||
if (userId) {
|
||||
const queryParams = rison.encode({
|
||||
filters: [
|
||||
{
|
||||
col: 'table_name',
|
||||
opr: 'ct',
|
||||
value: searchText,
|
||||
},
|
||||
{
|
||||
col: 'owners',
|
||||
opr: 'rel_m_m',
|
||||
value: userId,
|
||||
},
|
||||
],
|
||||
order_column: 'changed_on_delta_humanized',
|
||||
order_direction: 'desc',
|
||||
});
|
||||
|
||||
const response = await makeApi({
|
||||
method: 'GET',
|
||||
endpoint: '/api/v1/dataset',
|
||||
})(`q=${queryParams}`);
|
||||
|
||||
return response.result.map(
|
||||
(r: { table_name: string; id: number; owners: [DatasetOwner] }) => ({
|
||||
value: r.table_name,
|
||||
datasetId: r.id,
|
||||
owners: r.owners,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
handleSaveDatasetModalSearch = async (searchText: string) => {
|
||||
const userDatasetsOwned = await this.getUserDatasets(searchText);
|
||||
this.setState({ userDatasetOptions: userDatasetsOwned });
|
||||
};
|
||||
|
||||
handleFilterAutocompleteOption = (
|
||||
inputValue: string,
|
||||
option: { value: string; datasetId: number },
|
||||
) => option.value.toLowerCase().includes(inputValue.toLowerCase());
|
||||
|
||||
clearQueryResults(query: Query) {
|
||||
clearQueryResults(query: QueryResponse) {
|
||||
this.props.actions.clearQueryResults(query);
|
||||
}
|
||||
|
||||
@@ -477,11 +191,11 @@ export default class ResultSet extends React.PureComponent<
|
||||
this.setState({ searchText: event.target.value });
|
||||
}
|
||||
|
||||
fetchResults(query: Query) {
|
||||
fetchResults(query: QueryResponse) {
|
||||
this.props.actions.fetchQueryResults(query, this.props.displayLimit);
|
||||
}
|
||||
|
||||
reFetchQueryResults(query: Query) {
|
||||
reFetchQueryResults(query: QueryResponse) {
|
||||
this.props.actions.reFetchQueryResults(query);
|
||||
}
|
||||
|
||||
@@ -503,55 +217,31 @@ export default class ResultSet extends React.PureComponent<
|
||||
}
|
||||
const { columns } = this.props.query.results;
|
||||
// Added compute logic to stop user from being able to Save & Explore
|
||||
const {
|
||||
saveDatasetRadioBtnState,
|
||||
newSaveDatasetName,
|
||||
datasetToOverwrite,
|
||||
saveModalAutocompleteValue,
|
||||
shouldOverwriteDataSet,
|
||||
userDatasetOptions,
|
||||
showSaveDatasetModal,
|
||||
} = this.state;
|
||||
const disableSaveAndExploreBtn =
|
||||
(saveDatasetRadioBtnState === DatasetRadioState.SAVE_NEW &&
|
||||
newSaveDatasetName.length === 0) ||
|
||||
(saveDatasetRadioBtnState === DatasetRadioState.OVERWRITE_DATASET &&
|
||||
Object.keys(datasetToOverwrite).length === 0 &&
|
||||
saveModalAutocompleteValue.length === 0);
|
||||
const { showSaveDatasetModal } = this.state;
|
||||
const { query } = this.props;
|
||||
|
||||
return (
|
||||
<ResultSetControls>
|
||||
<SaveDatasetModal
|
||||
visible={showSaveDatasetModal}
|
||||
onOk={this.handleSaveInDataset}
|
||||
saveDatasetRadioBtnState={saveDatasetRadioBtnState}
|
||||
shouldOverwriteDataset={shouldOverwriteDataSet}
|
||||
defaultCreateDatasetValue={newSaveDatasetName}
|
||||
userDatasetOptions={userDatasetOptions}
|
||||
disableSaveAndExploreBtn={disableSaveAndExploreBtn}
|
||||
onHide={this.handleHideSaveModal}
|
||||
handleDatasetNameChange={this.handleDatasetNameChange}
|
||||
handleSaveDatasetRadioBtnState={this.handleSaveDatasetRadioBtnState}
|
||||
handleOverwriteCancel={this.handleOverwriteCancel}
|
||||
handleOverwriteDataset={this.handleOverwriteDataset}
|
||||
handleOverwriteDatasetOption={this.handleOverwriteDatasetOption}
|
||||
handleSaveDatasetModalSearch={this.handleSaveDatasetModalSearch}
|
||||
filterAutocompleteOption={this.handleFilterAutocompleteOption}
|
||||
onChangeAutoComplete={this.handleOnChangeAutoComplete}
|
||||
onHide={() => this.setState({ showSaveDatasetModal: false })}
|
||||
buttonTextOnSave={t('Save & Explore')}
|
||||
buttonTextOnOverwrite={t('Overwrite & Explore')}
|
||||
modalDescription={t(
|
||||
'Save this query as a virtual dataset to continue exploring',
|
||||
)}
|
||||
datasource={query}
|
||||
/>
|
||||
<ResultSetButtons>
|
||||
{this.props.visualize &&
|
||||
this.props.database?.allows_virtual_table_explore && (
|
||||
<ExploreResultsButton
|
||||
database={this.props.database}
|
||||
onClick={this.handleExploreBtnClick}
|
||||
onClick={() => this.setState({ showSaveDatasetModal: true })}
|
||||
/>
|
||||
)}
|
||||
{this.props.csv && (
|
||||
<Button
|
||||
buttonSize="small"
|
||||
href={`/superset/csv/${this.props.query.id}`}
|
||||
>
|
||||
<Button buttonSize="small" href={`/superset/csv/${query.id}`}>
|
||||
<i className="fa fa-file-text-o" /> {t('Download to CSV')}
|
||||
</Button>
|
||||
)}
|
||||
@@ -587,10 +277,6 @@ export default class ResultSet extends React.PureComponent<
|
||||
return <div />;
|
||||
}
|
||||
|
||||
onAlertClose = () => {
|
||||
this.setState({ alertIsOpen: false });
|
||||
};
|
||||
|
||||
renderRowsReturned() {
|
||||
const { results, rows, queryLimit, limitingFactor } = this.props.query;
|
||||
let limitMessage;
|
||||
@@ -646,17 +332,17 @@ export default class ResultSet extends React.PureComponent<
|
||||
return (
|
||||
<ReturnedRows>
|
||||
{!limitReached && !shouldUseDefaultDropdownAlert && (
|
||||
<ResultSetRowsReturned title={tooltipText}>
|
||||
<span title={tooltipText}>
|
||||
{rowsReturnedMessage}
|
||||
<LimitMessage>{limitMessage}</LimitMessage>
|
||||
</ResultSetRowsReturned>
|
||||
<span>{limitMessage}</span>
|
||||
</span>
|
||||
)}
|
||||
{!limitReached && shouldUseDefaultDropdownAlert && (
|
||||
<div ref={this.calculateAlertRefHeight}>
|
||||
<Alert
|
||||
type="warning"
|
||||
message={t('%(rows)d rows returned', { rows })}
|
||||
onClose={this.onAlertClose}
|
||||
onClose={() => this.setState({ alertIsOpen: false })}
|
||||
description={t(
|
||||
'The number of rows displayed is limited to %s by the dropdown.',
|
||||
rows,
|
||||
@@ -668,7 +354,7 @@ export default class ResultSet extends React.PureComponent<
|
||||
<div ref={this.calculateAlertRefHeight}>
|
||||
<Alert
|
||||
type="warning"
|
||||
onClose={this.onAlertClose}
|
||||
onClose={() => this.setState({ alertIsOpen: false })}
|
||||
message={t('%(rows)d rows returned', { rows: rowsCount })}
|
||||
description={
|
||||
isAdmin
|
||||
@@ -691,9 +377,7 @@ export default class ResultSet extends React.PureComponent<
|
||||
exploreDBId = this.props.database.explore_database_id;
|
||||
}
|
||||
|
||||
if (this.props.showSql) {
|
||||
sql = <HighlightedSql sql={query.sql} />;
|
||||
}
|
||||
if (this.props.showSql) sql = <HighlightedSql sql={query.sql} />;
|
||||
|
||||
if (query.state === 'stopped') {
|
||||
return <Alert type="warning" message={t('Query was stopped')} />;
|
||||
|
||||
@@ -17,44 +17,60 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Radio } from 'src/components/Radio';
|
||||
import { AutoComplete } from 'src/components';
|
||||
import { Input } from 'src/components/Input';
|
||||
import { QueryResponse, testQuery } from '@superset-ui/core';
|
||||
import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
|
||||
describe('SaveDatasetModal', () => {
|
||||
const mockedProps = {
|
||||
visible: false,
|
||||
onOk: () => {},
|
||||
onHide: () => {},
|
||||
handleDatasetNameChange: () => {},
|
||||
handleSaveDatasetRadioBtnState: () => {},
|
||||
saveDatasetRadioBtnState: 1,
|
||||
handleOverwriteCancel: () => {},
|
||||
handleOverwriteDataset: () => {},
|
||||
handleOverwriteDatasetOption: () => {},
|
||||
defaultCreateDatasetValue: 'someDatasets',
|
||||
shouldOverwriteDataset: false,
|
||||
userDatasetOptions: [],
|
||||
disableSaveAndExploreBtn: false,
|
||||
handleSaveDatasetModalSearch: () => Promise,
|
||||
filterAutocompleteOption: () => false,
|
||||
onChangeAutoComplete: () => {},
|
||||
};
|
||||
it('renders a radio group btn', () => {
|
||||
// @ts-ignore
|
||||
const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
|
||||
expect(wrapper.find(Radio.Group)).toExist();
|
||||
const mockedProps = {
|
||||
visible: true,
|
||||
onHide: () => {},
|
||||
buttonTextOnSave: 'Save',
|
||||
buttonTextOnOverwrite: 'Overwrite',
|
||||
datasource: testQuery as QueryResponse,
|
||||
};
|
||||
|
||||
describe('SaveDatasetModal RTL', () => {
|
||||
it('renders a "Save as new" field', () => {
|
||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||
|
||||
const saveRadioBtn = screen.getByRole('radio', {
|
||||
name: /save as new unimportant/i,
|
||||
});
|
||||
const fieldLabel = screen.getByText(/save as new/i);
|
||||
const inputField = screen.getByRole('textbox');
|
||||
const inputFieldText = screen.getByDisplayValue(/unimportant/i);
|
||||
|
||||
expect(saveRadioBtn).toBeVisible();
|
||||
expect(fieldLabel).toBeVisible();
|
||||
expect(inputField).toBeVisible();
|
||||
expect(inputFieldText).toBeVisible();
|
||||
});
|
||||
it('renders a autocomplete', () => {
|
||||
// @ts-ignore
|
||||
const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
|
||||
expect(wrapper.find(AutoComplete)).toExist();
|
||||
|
||||
it('renders an "Overwrite existing" field', () => {
|
||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||
|
||||
const overwriteRadioBtn = screen.getByRole('radio', {
|
||||
name: /overwrite existing select or type dataset name/i,
|
||||
});
|
||||
const fieldLabel = screen.getByText(/overwrite existing/i);
|
||||
const inputField = screen.getByRole('combobox');
|
||||
const placeholderText = screen.getByText(/select or type dataset name/i);
|
||||
|
||||
expect(overwriteRadioBtn).toBeVisible();
|
||||
expect(fieldLabel).toBeVisible();
|
||||
expect(inputField).toBeVisible();
|
||||
expect(placeholderText).toBeVisible();
|
||||
});
|
||||
it('renders an input form', () => {
|
||||
// @ts-ignore
|
||||
const wrapper = shallow(<SaveDatasetModal {...mockedProps} />);
|
||||
expect(wrapper.find(Input)).toExist();
|
||||
|
||||
it('renders a save button', () => {
|
||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeVisible();
|
||||
});
|
||||
|
||||
it('renders a close button', () => {
|
||||
render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
|
||||
|
||||
expect(screen.getByRole('button', { name: /close/i })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,153 +17,356 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import React, { FunctionComponent } from 'react';
|
||||
import { AutoCompleteProps } from 'antd/lib/auto-complete';
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { Radio } from 'src/components/Radio';
|
||||
import { AutoComplete, RadioChangeEvent } from 'src/components';
|
||||
import { Input } from 'src/components/Input';
|
||||
import StyledModal from 'src/components/Modal';
|
||||
import Button from 'src/components/Button';
|
||||
import { styled, t } from '@superset-ui/core';
|
||||
import {
|
||||
styled,
|
||||
t,
|
||||
SupersetClient,
|
||||
makeApi,
|
||||
JsonResponse,
|
||||
JsonObject,
|
||||
QueryResponse,
|
||||
} from '@superset-ui/core';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import moment from 'moment';
|
||||
import rison from 'rison';
|
||||
import { createDatasource } from 'src/SqlLab/actions/sqlLab';
|
||||
import { addDangerToast } from 'src/components/MessageToasts/actions';
|
||||
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
|
||||
import {
|
||||
DatasetRadioState,
|
||||
EXPLORE_CHART_DEFAULT,
|
||||
DatasetOwner,
|
||||
DatasetOptionAutocomplete,
|
||||
SqlLabExploreRootState,
|
||||
getInitialState,
|
||||
ExploreDatasource,
|
||||
} from 'src/SqlLab/types';
|
||||
import { exploreChart } from 'src/explore/exploreUtils';
|
||||
|
||||
interface SaveDatasetModalProps {
|
||||
visible: boolean;
|
||||
onOk: () => void;
|
||||
onHide: () => void;
|
||||
handleDatasetNameChange: (e: React.FormEvent<HTMLInputElement>) => void;
|
||||
handleSaveDatasetModalSearch: (searchText: string) => Promise<void>;
|
||||
filterAutocompleteOption: (
|
||||
inputValue: string,
|
||||
option: { value: string; datasetId: number },
|
||||
) => boolean;
|
||||
handleSaveDatasetRadioBtnState: (e: RadioChangeEvent) => void;
|
||||
handleOverwriteCancel: () => void;
|
||||
handleOverwriteDataset: () => void;
|
||||
handleOverwriteDatasetOption: (
|
||||
data: string,
|
||||
option: Record<string, any>,
|
||||
) => void;
|
||||
onChangeAutoComplete: () => void;
|
||||
defaultCreateDatasetValue: string;
|
||||
disableSaveAndExploreBtn: boolean;
|
||||
saveDatasetRadioBtnState: number;
|
||||
shouldOverwriteDataset: boolean;
|
||||
userDatasetOptions: AutoCompleteProps['options'];
|
||||
buttonTextOnSave: string;
|
||||
buttonTextOnOverwrite: string;
|
||||
modalDescription?: string;
|
||||
datasource: ExploreDatasource;
|
||||
}
|
||||
|
||||
const Styles = styled.div`
|
||||
.smd-body {
|
||||
.sdm-body {
|
||||
margin: 0 8px;
|
||||
}
|
||||
.smd-input {
|
||||
.sdm-input {
|
||||
margin-left: 45px;
|
||||
width: 401px;
|
||||
}
|
||||
.smd-autocomplete {
|
||||
.sdm-autocomplete {
|
||||
margin-left: 8px;
|
||||
width: 401px;
|
||||
}
|
||||
.smd-radio {
|
||||
.sdm-radio {
|
||||
display: block;
|
||||
height: 30px;
|
||||
margin: 10px 0px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.smd-overwrite-msg {
|
||||
.sdm-overwrite-msg {
|
||||
margin: 7px;
|
||||
}
|
||||
`;
|
||||
|
||||
const updateDataset = async (
|
||||
dbId: number,
|
||||
datasetId: number,
|
||||
sql: string,
|
||||
columns: Array<Record<string, any>>,
|
||||
owners: [number],
|
||||
overrideColumns: boolean,
|
||||
) => {
|
||||
const endpoint = `api/v1/dataset/${datasetId}?override_columns=${overrideColumns}`;
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
const body = JSON.stringify({
|
||||
sql,
|
||||
columns,
|
||||
owners,
|
||||
database_id: dbId,
|
||||
});
|
||||
|
||||
const data: JsonResponse = await SupersetClient.put({
|
||||
endpoint,
|
||||
headers,
|
||||
body,
|
||||
});
|
||||
return data.json.result;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
export const SaveDatasetModal: FunctionComponent<SaveDatasetModalProps> = ({
|
||||
visible,
|
||||
onOk,
|
||||
onHide,
|
||||
handleDatasetNameChange,
|
||||
handleSaveDatasetRadioBtnState,
|
||||
saveDatasetRadioBtnState,
|
||||
shouldOverwriteDataset,
|
||||
handleOverwriteCancel,
|
||||
handleOverwriteDataset,
|
||||
handleOverwriteDatasetOption,
|
||||
defaultCreateDatasetValue,
|
||||
disableSaveAndExploreBtn,
|
||||
handleSaveDatasetModalSearch,
|
||||
filterAutocompleteOption,
|
||||
userDatasetOptions,
|
||||
onChangeAutoComplete,
|
||||
}) => (
|
||||
<StyledModal
|
||||
show={visible}
|
||||
title="Save or Overwrite Dataset"
|
||||
onHide={onHide}
|
||||
footer={
|
||||
<>
|
||||
buttonTextOnSave,
|
||||
buttonTextOnOverwrite,
|
||||
modalDescription,
|
||||
datasource,
|
||||
}) => {
|
||||
const query = datasource as QueryResponse;
|
||||
const getDefaultDatasetName = () =>
|
||||
`${query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`;
|
||||
const [datasetName, setDatasetName] = useState(getDefaultDatasetName());
|
||||
const [newOrOverwrite, setNewOrOverwrite] = useState(
|
||||
DatasetRadioState.SAVE_NEW,
|
||||
);
|
||||
const [shouldOverwriteDataset, setShouldOverwriteDataset] = useState(false);
|
||||
const [userDatasetOptions, setUserDatasetOptions] = useState<
|
||||
DatasetOptionAutocomplete[]
|
||||
>([]);
|
||||
const [datasetToOverwrite, setDatasetToOverwrite] = useState<
|
||||
Record<string, any>
|
||||
>({});
|
||||
const [autocompleteValue, setAutocompleteValue] = useState('');
|
||||
|
||||
const user = useSelector<SqlLabExploreRootState, User>(user =>
|
||||
getInitialState(user),
|
||||
);
|
||||
const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
|
||||
|
||||
const handleOverwriteDataset = async () => {
|
||||
await updateDataset(
|
||||
query.dbId,
|
||||
datasetToOverwrite.datasetId,
|
||||
query.sql,
|
||||
query.results.selected_columns.map(
|
||||
(d: { name: string; type: string; is_dttm: boolean }) => ({
|
||||
column_name: d.name,
|
||||
type: d.type,
|
||||
is_dttm: d.is_dttm,
|
||||
}),
|
||||
),
|
||||
datasetToOverwrite.owners.map((o: DatasetOwner) => o.id),
|
||||
true,
|
||||
);
|
||||
|
||||
setShouldOverwriteDataset(false);
|
||||
setDatasetToOverwrite({});
|
||||
setDatasetName(getDefaultDatasetName());
|
||||
|
||||
exploreChart({
|
||||
...EXPLORE_CHART_DEFAULT,
|
||||
datasource: `${datasetToOverwrite.datasetId}__table`,
|
||||
all_columns: query.results.selected_columns.map(
|
||||
(d: { name: string; type: string; is_dttm: boolean }) => d.name,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const getUserDatasets = async (searchText = '') => {
|
||||
// Making sure that autocomplete input has a value before rendering the dropdown
|
||||
// Transforming the userDatasetsOwned data for SaveModalComponent)
|
||||
const { userId } = user;
|
||||
if (userId) {
|
||||
const queryParams = rison.encode({
|
||||
filters: [
|
||||
{
|
||||
col: 'table_name',
|
||||
opr: 'ct',
|
||||
value: searchText,
|
||||
},
|
||||
{
|
||||
col: 'owners',
|
||||
opr: 'rel_m_m',
|
||||
value: userId,
|
||||
},
|
||||
],
|
||||
order_column: 'changed_on_delta_humanized',
|
||||
order_direction: 'desc',
|
||||
});
|
||||
|
||||
const response = await makeApi({
|
||||
method: 'GET',
|
||||
endpoint: '/api/v1/dataset',
|
||||
})(`q=${queryParams}`);
|
||||
|
||||
return response.result.map(
|
||||
(r: { table_name: string; id: number; owners: [DatasetOwner] }) => ({
|
||||
value: r.table_name,
|
||||
datasetId: r.id,
|
||||
owners: r.owners,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSaveInDataset = () => {
|
||||
// if user wants to overwrite a dataset we need to prompt them
|
||||
if (newOrOverwrite === DatasetRadioState.OVERWRITE_DATASET) {
|
||||
setShouldOverwriteDataset(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedColumns = query.results.selected_columns || [];
|
||||
|
||||
// The filters param is only used to test jinja templates.
|
||||
// Remove the special filters entry from the templateParams
|
||||
// before saving the dataset.
|
||||
if (query.templateParams) {
|
||||
const p = JSON.parse(query.templateParams);
|
||||
/* eslint-disable-next-line no-underscore-dangle */
|
||||
if (p._filters) {
|
||||
/* eslint-disable-next-line no-underscore-dangle */
|
||||
delete p._filters;
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
query.templateParams = JSON.stringify(p);
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(
|
||||
createDatasource({
|
||||
schema: query.schema,
|
||||
sql: query.sql,
|
||||
dbId: query.dbId,
|
||||
templateParams: query.templateParams,
|
||||
datasourceName: datasetName,
|
||||
columns: selectedColumns,
|
||||
}),
|
||||
)
|
||||
.then((data: { table_id: number }) => {
|
||||
exploreChart({
|
||||
datasource: `${data.table_id}__table`,
|
||||
metrics: [],
|
||||
groupby: [],
|
||||
time_range: 'No filter',
|
||||
viz_type: 'table',
|
||||
all_columns: selectedColumns.map(c => c.name),
|
||||
row_limit: 1000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
addDangerToast(t('An error occurred saving dataset'));
|
||||
});
|
||||
|
||||
setDatasetName(getDefaultDatasetName());
|
||||
onHide();
|
||||
};
|
||||
|
||||
const handleSaveDatasetModalSearch = async (searchText: string) => {
|
||||
const userDatasetsOwned = await getUserDatasets(searchText);
|
||||
setUserDatasetOptions(userDatasetsOwned);
|
||||
};
|
||||
|
||||
const handleOverwriteDatasetOption = (
|
||||
_data: string,
|
||||
option: Record<string, any>,
|
||||
) => setDatasetToOverwrite(option);
|
||||
|
||||
const handleDatasetNameChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
// @ts-expect-error
|
||||
setDatasetName(e.target.value);
|
||||
};
|
||||
|
||||
const handleOverwriteCancel = () => {
|
||||
setShouldOverwriteDataset(false);
|
||||
setDatasetToOverwrite({});
|
||||
};
|
||||
|
||||
const disableSaveAndExploreBtn =
|
||||
(newOrOverwrite === DatasetRadioState.SAVE_NEW &&
|
||||
datasetName.length === 0) ||
|
||||
(newOrOverwrite === DatasetRadioState.OVERWRITE_DATASET &&
|
||||
Object.keys(datasetToOverwrite).length === 0 &&
|
||||
autocompleteValue.length === 0);
|
||||
|
||||
const filterAutocompleteOption = (
|
||||
inputValue: string,
|
||||
option: { value: string; datasetId: number },
|
||||
) => option.value.toLowerCase().includes(inputValue.toLowerCase());
|
||||
|
||||
return (
|
||||
<StyledModal
|
||||
show={visible}
|
||||
title={t('Save or Overwrite Dataset')}
|
||||
onHide={onHide}
|
||||
footer={
|
||||
<>
|
||||
{!shouldOverwriteDataset && (
|
||||
<Button
|
||||
disabled={disableSaveAndExploreBtn}
|
||||
buttonStyle="primary"
|
||||
onClick={handleSaveInDataset}
|
||||
>
|
||||
{buttonTextOnSave}
|
||||
</Button>
|
||||
)}
|
||||
{shouldOverwriteDataset && (
|
||||
<>
|
||||
<Button onClick={handleOverwriteCancel}>Back</Button>
|
||||
<Button
|
||||
className="md"
|
||||
buttonStyle="primary"
|
||||
onClick={handleOverwriteDataset}
|
||||
disabled={disableSaveAndExploreBtn}
|
||||
>
|
||||
{buttonTextOnOverwrite}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Styles>
|
||||
{!shouldOverwriteDataset && (
|
||||
<Button
|
||||
disabled={disableSaveAndExploreBtn}
|
||||
buttonStyle="primary"
|
||||
onClick={onOk}
|
||||
>
|
||||
{t('Save & Explore')}
|
||||
</Button>
|
||||
<div className="sdm-body">
|
||||
{modalDescription && (
|
||||
<div className="sdm-prompt">{modalDescription}</div>
|
||||
)}
|
||||
<Radio.Group
|
||||
onChange={(e: RadioChangeEvent) => {
|
||||
setNewOrOverwrite(Number(e.target.value));
|
||||
}}
|
||||
value={newOrOverwrite}
|
||||
>
|
||||
<Radio className="sdm-radio" value={1}>
|
||||
{t('Save as new')}
|
||||
<Input
|
||||
className="sdm-input"
|
||||
defaultValue={datasetName}
|
||||
onChange={handleDatasetNameChange}
|
||||
disabled={newOrOverwrite !== 1}
|
||||
/>
|
||||
</Radio>
|
||||
<Radio className="sdm-radio" value={2}>
|
||||
{t('Overwrite existing')}
|
||||
<AutoComplete
|
||||
className="sdm-autocomplete"
|
||||
options={userDatasetOptions}
|
||||
onSelect={handleOverwriteDatasetOption}
|
||||
onSearch={handleSaveDatasetModalSearch}
|
||||
onChange={value => {
|
||||
setDatasetToOverwrite({});
|
||||
setAutocompleteValue(value);
|
||||
}}
|
||||
placeholder={t('Select or type dataset name')}
|
||||
filterOption={filterAutocompleteOption}
|
||||
disabled={newOrOverwrite !== 2}
|
||||
value={autocompleteValue}
|
||||
/>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
)}
|
||||
{shouldOverwriteDataset && (
|
||||
<>
|
||||
<Button onClick={handleOverwriteCancel}>Back</Button>
|
||||
<Button
|
||||
className="md"
|
||||
buttonStyle="primary"
|
||||
onClick={handleOverwriteDataset}
|
||||
disabled={disableSaveAndExploreBtn}
|
||||
>
|
||||
{t('Overwrite & Explore')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Styles>
|
||||
{!shouldOverwriteDataset && (
|
||||
<div className="smd-body">
|
||||
<div className="smd-prompt">
|
||||
Save this query as a virtual dataset to continue exploring
|
||||
<div className="sdm-overwrite-msg">
|
||||
{t('Are you sure you want to overwrite this dataset?')}
|
||||
</div>
|
||||
<Radio.Group
|
||||
onChange={handleSaveDatasetRadioBtnState}
|
||||
value={saveDatasetRadioBtnState}
|
||||
>
|
||||
<Radio className="smd-radio" value={1}>
|
||||
Save as new
|
||||
<Input
|
||||
className="smd-input"
|
||||
defaultValue={defaultCreateDatasetValue}
|
||||
onChange={handleDatasetNameChange}
|
||||
disabled={saveDatasetRadioBtnState !== 1}
|
||||
/>
|
||||
</Radio>
|
||||
<Radio className="smd-radio" value={2}>
|
||||
Overwrite existing
|
||||
<AutoComplete
|
||||
className="smd-autocomplete"
|
||||
options={userDatasetOptions}
|
||||
onSelect={handleOverwriteDatasetOption}
|
||||
onSearch={handleSaveDatasetModalSearch}
|
||||
onChange={onChangeAutoComplete}
|
||||
placeholder="Select or type dataset name"
|
||||
filterOption={filterAutocompleteOption}
|
||||
disabled={saveDatasetRadioBtnState !== 2}
|
||||
/>
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
)}
|
||||
{shouldOverwriteDataset && (
|
||||
<div className="smd-overwrite-msg">
|
||||
Are you sure you want to overwrite this dataset?
|
||||
</div>
|
||||
)}
|
||||
</Styles>
|
||||
</StyledModal>
|
||||
);
|
||||
)}
|
||||
</Styles>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { QueryState } from 'src/SqlLab/types';
|
||||
import { QueryState } from '@superset-ui/core';
|
||||
|
||||
interface TabStatusIconProps {
|
||||
tabState: QueryState;
|
||||
|
||||
Reference in New Issue
Block a user