/** * 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 { useCallback, useState, FormEvent } from 'react'; import { Radio, RadioChangeEvent } from 'src/components/Radio'; import { AsyncSelect } from 'src/components'; import { Input } from 'src/components/Input'; import StyledModal from 'src/components/Modal'; import Button from 'src/components/Button'; import { styled, t, SupersetClient, JsonResponse, JsonObject, QueryResponse, QueryFormData, VizType, } from '@superset-ui/core'; import { useSelector, useDispatch } from 'react-redux'; import dayjs from 'dayjs'; 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, SqlLabRootState, } from 'src/SqlLab/types'; import { mountExploreUrl } from 'src/explore/exploreUtils'; import { postFormData } from 'src/explore/exploreUtils/formData'; import { URL_PARAMS } from 'src/constants'; // eslint-disable-next-line no-restricted-imports import { SelectValue } from 'antd/lib/select'; // TODO: Remove antd import { isEmpty } from 'lodash'; interface QueryDatabase { id?: number; } export type ExploreQuery = QueryResponse & { database?: QueryDatabase | null | undefined; }; export interface ISimpleColumn { column_name?: string | null; name?: string | null; type?: string | null; is_dttm?: boolean | null; } export type Database = { backend: string; id: number; parameter: object; }; export interface ISaveableDatasource { columns: ISimpleColumn[]; name: string; dbId: number; sql: string; templateParams?: string | object | null; catalog?: string | null; schema?: string | null; database?: Database; } interface SaveDatasetModalProps { visible: boolean; onHide: () => void; buttonTextOnSave: string; buttonTextOnOverwrite: string; modalDescription?: string; datasource: ISaveableDatasource; openWindow?: boolean; formData?: Omit; } const Styles = styled.div` ${({ theme }) => ` .sdm-body { margin: 0 ${theme.gridUnit * 2}px; } .sdm-input { margin-left: ${theme.gridUnit * 10}px; width: 401px; } .sdm-autocomplete { width: 401px; align-self: center; margin-left: ${theme.gridUnit}px; } .sdm-radio { height: 30px; margin: 10px 0px; line-height: 30px; } .sdm-radio span { display: inline-flex; padding-right: 0px; } .sdm-overwrite-msg { margin: ${theme.gridUnit * 2}px; } .sdm-overwrite-container { flex: 1 1 auto; display: flex; `} `; const updateDataset = async ( dbId: number, datasetId: number, sql: string, columns: Array>, 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; }; const UNTITLED = t('Untitled Dataset'); export const SaveDatasetModal = ({ visible, onHide, buttonTextOnSave, buttonTextOnOverwrite, modalDescription, datasource, openWindow = true, formData = {}, }: SaveDatasetModalProps) => { const defaultVizType = useSelector( state => state.common?.conf?.DEFAULT_VIZ_TYPE || VizType.Table, ); const getDefaultDatasetName = () => `${datasource?.name || UNTITLED} ${dayjs().format('L HH:mm:ss')}`; const [datasetName, setDatasetName] = useState(getDefaultDatasetName()); const [newOrOverwrite, setNewOrOverwrite] = useState( DatasetRadioState.SaveNew, ); const [shouldOverwriteDataset, setShouldOverwriteDataset] = useState(false); const [datasetToOverwrite, setDatasetToOverwrite] = useState< Record >({}); const [selectedDatasetToOverwrite, setSelectedDatasetToOverwrite] = useState< SelectValue | undefined >(undefined); const [loading, setLoading] = useState(false); const user = useSelector(state => state.user); const dispatch = useDispatch<(dispatch: any) => Promise>(); const createWindow = (url: string) => { if (openWindow) { window.open(url, '_blank', 'noreferrer'); } else { window.location.href = url; } }; const formDataWithDefaults = { ...EXPLORE_CHART_DEFAULT, ...(formData || {}), }; const handleOverwriteDataset = async () => { // if user wants to overwrite a dataset we need to prompt them if (!shouldOverwriteDataset) { setShouldOverwriteDataset(true); return; } setLoading(true); const [, key] = await Promise.all([ updateDataset( datasource?.dbId, datasetToOverwrite?.datasetid, datasource?.sql, datasource?.columns?.map( (d: { column_name: string; type: string; is_dttm: boolean }) => ({ column_name: d.column_name, type: d.type, is_dttm: d.is_dttm, }), ), datasetToOverwrite?.owners?.map((o: DatasetOwner) => o.id), true, ), postFormData(datasetToOverwrite.datasetid, 'table', { ...formDataWithDefaults, datasource: `${datasetToOverwrite.datasetid}__table`, ...(defaultVizType === VizType.Table && { all_columns: datasource?.columns?.map(column => column.column_name), }), }), ]); setLoading(false); const url = mountExploreUrl(null, { [URL_PARAMS.formDataKey.name]: key, }); createWindow(url); setShouldOverwriteDataset(false); setDatasetName(getDefaultDatasetName()); onHide(); }; const loadDatasetOverwriteOptions = useCallback( async (input = '') => { const { userId } = user; const queryParams = rison.encode({ filters: [ { col: 'table_name', opr: 'ct', value: input, }, { col: 'owners', opr: 'rel_m_m', value: userId, }, ], order_column: 'changed_on_delta_humanized', order_direction: 'desc', }); return SupersetClient.get({ endpoint: `/api/v1/dataset/?q=${queryParams}`, }).then(response => ({ data: response.json.result.map( (r: { table_name: string; id: number; owners: [DatasetOwner] }) => ({ value: r.table_name, label: r.table_name, datasetid: r.id, owners: r.owners, }), ), totalCount: response.json.count, })); }, [user], ); const handleSaveInDataset = () => { setLoading(true); const selectedColumns = datasource?.columns ?? []; // The filters param is only used to test jinja templates. // Remove the special filters entry from the templateParams // before saving the dataset. let templateParams; if (typeof datasource?.templateParams === 'string') { const p = JSON.parse(datasource.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 templateParams = JSON.stringify(p); } } dispatch( createDatasource({ sql: datasource.sql, dbId: datasource.dbId || datasource?.database?.id, catalog: datasource?.catalog, schema: datasource?.schema, templateParams, datasourceName: datasetName, }), ) .then((data: { id: number }) => postFormData(data.id, 'table', { ...formDataWithDefaults, datasource: `${data.id}__table`, ...(defaultVizType === VizType.Table && { all_columns: selectedColumns.map(column => column.column_name), }), }), ) .then((key: string) => { setLoading(false); const url = mountExploreUrl(null, { [URL_PARAMS.formDataKey.name]: key, }); createWindow(url); setDatasetName(getDefaultDatasetName()); onHide(); }) .catch(() => { setLoading(false); addDangerToast(t('An error occurred saving dataset')); }); }; const handleOverwriteDatasetOption = (value: SelectValue, option: any) => { setDatasetToOverwrite(option); setSelectedDatasetToOverwrite(value); }; const handleDatasetNameChange = (e: FormEvent) => { // @ts-expect-error setDatasetName(e.target.value); }; const handleOverwriteCancel = () => { setShouldOverwriteDataset(false); setDatasetToOverwrite({}); }; const disableSaveAndExploreBtn = (newOrOverwrite === DatasetRadioState.SaveNew && datasetName.length === 0) || (newOrOverwrite === DatasetRadioState.OverwriteDataset && isEmpty(selectedDatasetToOverwrite)); const filterAutocompleteOption = ( inputValue: string, option: { value: string; datasetid: number }, ) => option.value.toLowerCase().includes(inputValue.toLowerCase()); return ( {newOrOverwrite === DatasetRadioState.SaveNew && ( )} {newOrOverwrite === DatasetRadioState.OverwriteDataset && ( <> {shouldOverwriteDataset && ( )} )} } > {!shouldOverwriteDataset && (
{modalDescription && (
{modalDescription}
)} { setNewOrOverwrite(Number(e.target.value)); }} value={newOrOverwrite} > {t('Save as new')}
{t('Overwrite existing')}
loadDatasetOverwriteOptions(input)} value={selectedDatasetToOverwrite} filterOption={filterAutocompleteOption} disabled={newOrOverwrite !== 2} getPopupContainer={() => document.body} />
)} {shouldOverwriteDataset && (
{t('Are you sure you want to overwrite this dataset?')}
)}
); };