Files
superset2/superset-frontend/src/SqlLab/components/SaveQuery/index.tsx
T
Enzo MartellucciandClaude Sonnet 5 5a6c1b977b refactor(sqllab): drop the redundant canSaveDataset prop from SaveQuery
SaveQuery already receives the query's result columns and computes its
own SQL-staleness check, so master's canSaveDataset prop (threaded
through SqlEditor -> SaveQuery -> SaveDatasetActionButton) duplicated
the same "did the query succeed" condition. Fold it into the single
check SaveQuery already owns instead of ANDing two independently
computed booleans together.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 14:17:12 +02:00

284 lines
8.0 KiB
TypeScript

/**
* 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 { useState, useEffect, useMemo, ChangeEvent } from 'react';
import { useSelector } from 'react-redux';
import { Query, QueryState } from '@superset-ui/core';
import type { DatabaseObject } from 'src/features/databases/types';
import { t } from '@apache-superset/core/translation';
import { styled } from '@apache-superset/core/theme';
import {
Input,
Button,
Form,
FormItem,
Modal,
Row,
Col,
Icons,
} from '@superset-ui/core/components';
import SaveDatasetActionButton from 'src/SqlLab/components/SaveDatasetActionButton';
import {
SaveDatasetModal,
ISaveableDatasource,
} from 'src/SqlLab/components/SaveDatasetModal';
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
import { QueryEditor, SqlLabRootState } from 'src/SqlLab/types';
import useLogAction from 'src/logger/useLogAction';
import {
LOG_ACTIONS_SQLLAB_CREATE_CHART,
LOG_ACTIONS_SQLLAB_SAVE_QUERY,
} from 'src/logger/LogUtils';
import { ModalTitleWithIcon } from 'src/components/ModalTitleWithIcon';
interface SaveQueryProps {
queryEditorId: string;
columns: ISaveableDatasource['columns'];
onSave: (arg0: QueryPayload, id: string) => void;
onUpdate: (arg0: QueryPayload, id: string) => void;
saveQueryWarning: string | null;
database: Partial<DatabaseObject> | undefined;
}
export type QueryPayload = {
name: string;
description?: string;
id?: string;
remoteId?: number;
} & Pick<QueryEditor, 'dbId' | 'catalog' | 'schema' | 'sql'>;
const Styles = styled.span`
display: contents;
white-space: nowrap;
span[role='img']:not([aria-label='down']) {
display: flex;
margin: 0;
svg {
vertical-align: -${({ theme }) => theme.sizeUnit * 1.25}px;
margin: 0;
}
}
`;
const SaveQuery = ({
queryEditorId,
onSave = () => {},
onUpdate,
saveQueryWarning,
database,
columns,
}: SaveQueryProps) => {
const queryEditor = useQueryEditor(queryEditorId, [
'autorun',
'name',
'description',
'remoteId',
'dbId',
'latestQueryId',
'queryLimit',
'catalog',
'schema',
'selectedText',
'sql',
'templateParams',
]);
const query = useMemo(
() => ({
...queryEditor,
columns,
}),
[queryEditor, columns],
);
const logAction = useLogAction({ queryEditorId });
const defaultLabel = query.name || query.description || t('Undefined');
const [description, setDescription] = useState<string>(
query.description || '',
);
const [label, setLabel] = useState<string>(defaultLabel);
const [showSave, setShowSave] = useState<boolean>(false);
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
// Saving a dataset runs the SQL to introspect columns, so it needs a
// successful run of the SQL being saved that produced at least one column
// -- editing after a run invalidates it, and running a selection only
// validates that selection.
const latestQuery = useSelector<SqlLabRootState, Query | undefined>(
({ sqlLab }) => sqlLab.queries[queryEditor.latestQueryId || ''],
);
const canSaveDataset =
latestQuery?.state === QueryState.Success &&
latestQuery.sql === queryEditor.sql &&
columns.length > 0;
const isSaved = !!query.remoteId;
const isLabelEmpty = label.trim().length === 0;
const canExploreDatabase = !!database?.allows_virtual_table_explore;
const shouldShowSaveButton =
database?.allows_virtual_table_explore !== undefined;
const onSaveAsExplore = () => {
logAction(LOG_ACTIONS_SQLLAB_CREATE_CHART, {});
setShowSaveDatasetModal(true);
};
const queryPayload = () => ({
name: label,
description,
dbId: query.dbId ?? 0,
sql: query.sql,
catalog: query.catalog,
schema: query.schema,
templateParams: query.templateParams,
remoteId: query?.remoteId || undefined,
});
useEffect(() => {
if (!isSaved) setLabel(defaultLabel);
}, [defaultLabel]);
const close = () => setShowSave(false);
const onSaveWrapper = async () => {
logAction(LOG_ACTIONS_SQLLAB_SAVE_QUERY, {});
await onSave(queryPayload(), query.id);
close();
};
const onUpdateWrapper = async () => {
await onUpdate(queryPayload(), query.id);
close();
};
const onLabelChange = (e: ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
};
const onDescriptionChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setDescription(e.target.value);
};
const renderModalBody = () => (
<Form layout="vertical">
<Row>
<Col xs={24}>
<FormItem label={t('Name')} htmlFor="save-query-name">
<Input
id="save-query-name"
type="text"
value={label}
onChange={onLabelChange}
/>
</FormItem>
</Col>
</Row>
<br />
<Row>
<Col xs={24}>
<FormItem label={t('Description')} htmlFor="save-query-description">
<Input.TextArea
id="save-query-description"
rows={4}
value={description}
onChange={onDescriptionChange}
/>
</FormItem>
</Col>
</Row>
{saveQueryWarning && (
<>
<br />
<div>
<Row>
<Col xs={24}>
<small>{saveQueryWarning}</small>
</Col>
</Row>
<br />
</div>
</>
)}
</Form>
);
return (
<Styles className="SaveQuery">
{shouldShowSaveButton && (
<SaveDatasetActionButton
setShowSave={setShowSave}
onSaveAsExplore={canExploreDatabase ? onSaveAsExplore : undefined}
canSaveDataset={canSaveDataset}
/>
)}
<SaveDatasetModal
visible={showSaveDatasetModal}
onHide={() => setShowSaveDatasetModal(false)}
buttonTextOnSave={t('Save & Explore')}
buttonTextOnOverwrite={t('Overwrite & Explore')}
datasource={getDatasourceAsSaveableDataset(query)}
/>
<Modal
className="save-query-modal"
onHide={close}
width="620px"
show={showSave}
name={t('Save query')}
title={
<ModalTitleWithIcon
title={t('Save query')}
icon={<Icons.SaveOutlined />}
data-test="save-query-modal-title"
/>
}
footer={
<>
<Button
onClick={close}
data-test="cancel-query"
cta
buttonStyle="secondary"
>
{t('Cancel')}
</Button>
<Button
buttonStyle={isSaved ? 'secondary' : 'primary'}
onClick={onSaveWrapper}
disabled={isLabelEmpty}
cta
>
{isSaved ? t('Save as new') : t('Save')}
</Button>
{isSaved && (
<Button
buttonStyle="primary"
onClick={onUpdateWrapper}
disabled={isLabelEmpty}
cta
>
{t('Update')}
</Button>
)}
</>
}
>
{renderModalBody()}
</Modal>
</Styles>
);
};
export default SaveQuery;