mirror of
https://github.com/apache/superset.git
synced 2026-04-17 07:05:04 +00:00
feat(Dataset): editor improvements - run in sqllab (#33443)
This commit is contained in:
@@ -115,6 +115,7 @@ import {
|
||||
LOG_ACTIONS_SQLLAB_STOP_QUERY,
|
||||
Logger,
|
||||
} from 'src/logger/LogUtils';
|
||||
import CopyToClipboard from 'src/components/CopyToClipboard';
|
||||
import TemplateParamsEditor from '../TemplateParamsEditor';
|
||||
import SouthPane from '../SouthPane';
|
||||
import SaveQuery, { QueryPayload } from '../SaveQuery';
|
||||
@@ -315,6 +316,7 @@ const SqlEditor: FC<Props> = ({
|
||||
);
|
||||
const [showCreateAsModal, setShowCreateAsModal] = useState(false);
|
||||
const [createAs, setCreateAs] = useState('');
|
||||
const currentSQL = useRef<string>(queryEditor.sql);
|
||||
const showEmptyState = useMemo(
|
||||
() => !database || isEmpty(database),
|
||||
[database],
|
||||
@@ -648,6 +650,7 @@ const SqlEditor: FC<Props> = ({
|
||||
);
|
||||
|
||||
const onSqlChanged = useEffectEvent((sql: string) => {
|
||||
currentSQL.current = sql;
|
||||
dispatch(queryEditorSetSql(queryEditor, sql));
|
||||
});
|
||||
|
||||
@@ -890,6 +893,73 @@ const SqlEditor: FC<Props> = ({
|
||||
dispatch(queryEditorSetCursorPosition(queryEditor, newPosition));
|
||||
};
|
||||
|
||||
const copyQuery = (callback: (text: string) => void) => {
|
||||
callback(currentSQL.current);
|
||||
};
|
||||
const renderCopyQueryButton = () => (
|
||||
<Button type="primary">{t('COPY QUERY')}</Button>
|
||||
);
|
||||
|
||||
const renderDatasetWarning = () => (
|
||||
<Alert
|
||||
css={css`
|
||||
margin-bottom: ${theme.gridUnit * 2}px;
|
||||
padding-top: ${theme.gridUnit * 4}px;
|
||||
.antd5-alert-action {
|
||||
align-self: center;
|
||||
}
|
||||
`}
|
||||
type="info"
|
||||
action={
|
||||
<CopyToClipboard
|
||||
wrapText={false}
|
||||
copyNode={renderCopyQueryButton()}
|
||||
getText={copyQuery}
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`}
|
||||
>
|
||||
<p
|
||||
css={css`
|
||||
font-size: ${theme.typography.sizes.m}px;
|
||||
font-weight: ${theme.typography.weights.medium};
|
||||
color: ${theme.colors.primary.dark2};
|
||||
`}
|
||||
>
|
||||
{' '}
|
||||
{t(`You are edting a query from the virtual dataset `) +
|
||||
queryEditor.name}
|
||||
</p>
|
||||
<p
|
||||
css={css`
|
||||
font-size: ${theme.typography.sizes.m}px;
|
||||
font-weight: ${theme.typography.weights.normal};
|
||||
color: ${theme.colors.primary.dark2};
|
||||
`}
|
||||
>
|
||||
{t(
|
||||
'After making the changes, copy the query and paste in the virtual dataset SQL snippet settings.',
|
||||
)}{' '}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
message=""
|
||||
/>
|
||||
);
|
||||
|
||||
const queryPane = () => {
|
||||
const { aceEditorHeight, southPaneHeight } =
|
||||
getAceEditorAndSouthPaneHeights(height, northPercent, southPercent);
|
||||
@@ -899,7 +969,7 @@ const SqlEditor: FC<Props> = ({
|
||||
className="queryPane"
|
||||
sizes={[northPercent, southPercent]}
|
||||
elementStyle={elementStyle}
|
||||
minSize={200}
|
||||
minSize={queryEditor.isDataset ? 400 : 200}
|
||||
direction="vertical"
|
||||
gutterSize={SQL_EDITOR_GUTTER_HEIGHT}
|
||||
onDragStart={onResizeStart}
|
||||
@@ -915,6 +985,7 @@ const SqlEditor: FC<Props> = ({
|
||||
startQuery={startQuery}
|
||||
/>
|
||||
)}
|
||||
{queryEditor.isDataset && renderDatasetWarning()}
|
||||
{isActive && (
|
||||
<AceEditorWrapper
|
||||
autocomplete={autocompleteEnabled && !isTempId(queryEditor.id)}
|
||||
|
||||
@@ -141,6 +141,7 @@ class TabbedSqlEditors extends PureComponent<TabbedSqlEditorsProps> {
|
||||
schema,
|
||||
autorun,
|
||||
sql,
|
||||
isDataset: this.context.isDataset,
|
||||
};
|
||||
this.props.actions.addQueryEditor(newQueryEditor);
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface QueryEditor {
|
||||
southPercent?: number;
|
||||
updatedAt?: number;
|
||||
cursorPosition?: CursorPosition;
|
||||
isDataset?: boolean;
|
||||
}
|
||||
|
||||
export type toastState = {
|
||||
|
||||
@@ -149,14 +149,6 @@ const StyledButtonWrapper = styled.span`
|
||||
`}
|
||||
`;
|
||||
|
||||
const sqlTooltipOptions = {
|
||||
placement: 'topRight',
|
||||
title: t(
|
||||
'If changes are made to your SQL query, ' +
|
||||
'columns in your dataset will be synced when saving the dataset.',
|
||||
),
|
||||
};
|
||||
|
||||
const checkboxGenerator = (d, onChange) => (
|
||||
<CheckboxControl value={d} onChange={onChange} />
|
||||
);
|
||||
@@ -723,6 +715,22 @@ class DatasourceEditor extends PureComponent {
|
||||
});
|
||||
}
|
||||
|
||||
getSQLLabUrl() {
|
||||
const queryParams = new URLSearchParams({
|
||||
dbid: this.state.datasource.database.id,
|
||||
sql: this.state.datasource.sql,
|
||||
name: this.state.datasource.datasource_name,
|
||||
schema: this.state.datasource.schema,
|
||||
autorun: true,
|
||||
isDataset: true,
|
||||
});
|
||||
return `/sqllab/?${queryParams.toString()}`;
|
||||
}
|
||||
|
||||
openOnSqlLab() {
|
||||
window.open(this.getSQLLabUrl(), '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
tableChangeAndSyncMetadata() {
|
||||
this.validate(() => {
|
||||
this.syncMetadata();
|
||||
@@ -996,8 +1004,81 @@ class DatasourceEditor extends PureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderSqlEditorOverlay = () => (
|
||||
<div
|
||||
css={theme => css`
|
||||
position: absolute;
|
||||
background: ${theme.colors.secondary.light5};
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
`}
|
||||
>
|
||||
<div>
|
||||
<Loading position="inline-centered" />
|
||||
<span
|
||||
css={theme => css`
|
||||
display: block;
|
||||
margin: ${theme.gridUnit * 4}px auto;
|
||||
width: fit-content;
|
||||
color: ${theme.colors.grayscale.base};
|
||||
`}
|
||||
>
|
||||
{t('We are working on your query')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
renderOpenInSqlLabLink(isError = false) {
|
||||
return (
|
||||
<a
|
||||
href={this.getSQLLabUrl()}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
css={theme => css`
|
||||
color: ${isError
|
||||
? theme.colors.error.base
|
||||
: theme.colors.grayscale.base};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
text-decoration: underline;
|
||||
`}
|
||||
>
|
||||
{t('Open in SQL lab')}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
renderSqlErrorMessage = () => (
|
||||
<>
|
||||
<span
|
||||
css={theme => css`
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
`}
|
||||
>
|
||||
{this.props.database?.error && t('Error executing query. ')}
|
||||
</span>
|
||||
{this.renderOpenInSqlLabLink(true)}
|
||||
<span
|
||||
css={theme => css`
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
`}
|
||||
>
|
||||
{t(' to check for details.')}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
renderSourceFieldset() {
|
||||
const { datasource } = this.state;
|
||||
const floatingButtonCss = css`
|
||||
align-self: flex-end;
|
||||
height: 24px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
`;
|
||||
return (
|
||||
<div>
|
||||
<EditLockContainer>
|
||||
@@ -1097,18 +1178,33 @@ class DatasourceEditor extends PureComponent {
|
||||
description={t(
|
||||
'When specifying SQL, the datasource acts as a view. ' +
|
||||
'Superset will use this statement as a subquery while grouping and filtering ' +
|
||||
'on the generated parent queries.',
|
||||
'on the generated parent queries.' +
|
||||
'If changes are made to your SQL query, ' +
|
||||
'columns in your dataset will be synced when saving the dataset.',
|
||||
)}
|
||||
control={
|
||||
<TextAreaControl
|
||||
language="sql"
|
||||
offerEditInModal={false}
|
||||
minLines={10}
|
||||
maxLines={Infinity}
|
||||
readOnly={!this.state.isEditMode}
|
||||
resize="both"
|
||||
tooltipOptions={sqlTooltipOptions}
|
||||
/>
|
||||
this.props.database?.isLoading ? (
|
||||
<>
|
||||
{this.renderSqlEditorOverlay()}
|
||||
<TextAreaControl
|
||||
language="sql"
|
||||
offerEditInModal={false}
|
||||
minLines={10}
|
||||
maxLines={Infinity}
|
||||
readOnly={!this.state.isEditMode}
|
||||
resize="both"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<TextAreaControl
|
||||
language="sql"
|
||||
offerEditInModal={false}
|
||||
minLines={10}
|
||||
maxLines={Infinity}
|
||||
readOnly={!this.state.isEditMode}
|
||||
resize="both"
|
||||
/>
|
||||
)
|
||||
}
|
||||
additionalControl={
|
||||
<div
|
||||
@@ -1120,12 +1216,25 @@ class DatasourceEditor extends PureComponent {
|
||||
`}
|
||||
>
|
||||
<Button
|
||||
css={css`
|
||||
align-self: flex-end;
|
||||
height: 24px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
`}
|
||||
disabled={this.props.database?.isLoading}
|
||||
tooltip={t('Open SQL Lab in a new tab')}
|
||||
css={floatingButtonCss}
|
||||
size="small"
|
||||
>
|
||||
<Icons.ExportOutlined
|
||||
iconSize="s"
|
||||
css={theme => ({
|
||||
color: theme.colors.primary.dark1,
|
||||
})}
|
||||
onClick={() => {
|
||||
this.openOnSqlLab();
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
disabled={this.props.database?.isLoading}
|
||||
tooltip={t('Run query')}
|
||||
css={floatingButtonCss}
|
||||
size="small"
|
||||
buttonStyle="primary"
|
||||
onClick={() => {
|
||||
@@ -1142,22 +1251,49 @@ class DatasourceEditor extends PureComponent {
|
||||
</div>
|
||||
}
|
||||
errorMessage={
|
||||
this.props.database?.error && t('Error executing query.')
|
||||
this.props.database?.error && this.renderSqlErrorMessage()
|
||||
}
|
||||
/>
|
||||
{this.props.database?.queryResult && (
|
||||
<ResultTable
|
||||
data={this.props.database.queryResult.data}
|
||||
queryId={this.props.database.queryResult.query.id}
|
||||
orderedColumnKeys={this.props.database.queryResult.columns.map(
|
||||
col => col.column_name,
|
||||
)}
|
||||
height={100}
|
||||
expandedColumns={
|
||||
this.props.database.queryResult.expandedColumns
|
||||
}
|
||||
allowHTML
|
||||
/>
|
||||
<>
|
||||
<div
|
||||
css={theme => css`
|
||||
margin-bottom: ${theme.gridUnit * 4}px;
|
||||
`}
|
||||
>
|
||||
<span
|
||||
css={theme => css`
|
||||
color: ${theme.colors.grayscale.base};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
`}
|
||||
>
|
||||
{t(
|
||||
'In this view you can preview the first 25 rows. ',
|
||||
)}
|
||||
</span>
|
||||
{this.renderOpenInSqlLabLink()}
|
||||
<span
|
||||
css={theme => css`
|
||||
color: ${theme.colors.grayscale.base};
|
||||
font-size: ${theme.typography.sizes.s}px;
|
||||
`}
|
||||
>
|
||||
{t(' to see details.')}
|
||||
</span>
|
||||
</div>
|
||||
<ResultTable
|
||||
data={this.props.database?.queryResult.data}
|
||||
queryId={this.props.database?.queryResult.query.id}
|
||||
orderedColumnKeys={this.props.database?.queryResult.columns.map(
|
||||
col => col.column_name,
|
||||
)}
|
||||
height={100}
|
||||
expandedColumns={
|
||||
this.props.database?.queryResult.expandedColumns
|
||||
}
|
||||
allowHTML
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -1555,8 +1691,7 @@ const mapDispatchToProps = dispatch => ({
|
||||
resetQuery: () => dispatch(resetDatabaseState()),
|
||||
});
|
||||
const mapStateToProps = state => ({
|
||||
test: state.queryApi,
|
||||
database: state.database,
|
||||
database: state?.database,
|
||||
});
|
||||
export default withToasts(
|
||||
connect(mapStateToProps, mapDispatchToProps)(DataSourceComponent),
|
||||
|
||||
@@ -45,12 +45,17 @@ const props = {
|
||||
},
|
||||
};
|
||||
const DATASOURCE_ENDPOINT = 'glob:*/datasource/external_metadata_by_name/*';
|
||||
|
||||
const routeProps = {
|
||||
history: {},
|
||||
location: {},
|
||||
match: {},
|
||||
};
|
||||
const asyncRender = props =>
|
||||
waitFor(() =>
|
||||
render(<DatasourceEditor {...props} />, {
|
||||
render(<DatasourceEditor {...props} {...routeProps} />, {
|
||||
useRedux: true,
|
||||
initialState: { common: { currencies: ['USD', 'GBP', 'EUR'] } },
|
||||
useRouter: true,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -49,11 +49,15 @@ const mockedProps = {
|
||||
};
|
||||
|
||||
let container;
|
||||
|
||||
const routeProps = {
|
||||
history: {},
|
||||
location: {},
|
||||
match: {},
|
||||
};
|
||||
async function renderAndWait(props = mockedProps) {
|
||||
const { container: renderedContainer } = render(
|
||||
<DatasourceModal {...props} />,
|
||||
{ store },
|
||||
<DatasourceModal {...props} {...routeProps} />,
|
||||
{ store, useRouter: true },
|
||||
);
|
||||
|
||||
container = renderedContainer;
|
||||
|
||||
@@ -39,7 +39,7 @@ interface FieldProps<V> {
|
||||
onChange: (fieldKey: string, newValue: V) => void;
|
||||
compact: boolean;
|
||||
inline: boolean;
|
||||
errorMessage?: string;
|
||||
errorMessage?: string | ReactElement;
|
||||
}
|
||||
|
||||
export default function Field<V>({
|
||||
|
||||
@@ -28,10 +28,10 @@ export function recurseReactClone(children, type, propExtender) {
|
||||
*/
|
||||
return Children.map(children, child => {
|
||||
let newChild = child;
|
||||
if (child && child.type.name === type.name) {
|
||||
if (child && child.type && child.type.name === type.name) {
|
||||
newChild = cloneElement(child, propExtender(child));
|
||||
}
|
||||
if (newChild && newChild.props.children) {
|
||||
if (newChild && newChild.props && newChild.props.children) {
|
||||
newChild = cloneElement(newChild, {
|
||||
children: recurseReactClone(
|
||||
newChild.props.children,
|
||||
|
||||
@@ -116,6 +116,7 @@ import {
|
||||
UnorderedListOutlined,
|
||||
WarningOutlined,
|
||||
KeyOutlined,
|
||||
ExportOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { FC } from 'react';
|
||||
import { IconType } from './types';
|
||||
@@ -221,6 +222,7 @@ const AntdIcons = {
|
||||
UnorderedListOutlined,
|
||||
WarningOutlined,
|
||||
KeyOutlined,
|
||||
ExportOutlined,
|
||||
} as const;
|
||||
|
||||
type AntdIconNames = keyof typeof AntdIcons;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { useLocation } from 'react-router-dom';
|
||||
|
||||
export type LocationState = {
|
||||
requestedQuery?: Record<string, any>;
|
||||
isDataset?: boolean;
|
||||
};
|
||||
|
||||
export const locationContext = createContext<LocationState>({});
|
||||
@@ -32,7 +33,24 @@ const EMPTY_STATE: LocationState = {};
|
||||
|
||||
export const LocationProvider: FC = ({ children }: { children: ReactNode }) => {
|
||||
const location = useLocation<LocationState>();
|
||||
return <Provider value={location.state || EMPTY_STATE}>{children}</Provider>;
|
||||
};
|
||||
if (location.state) {
|
||||
return <Provider value={location.state}>{children}</Provider>;
|
||||
}
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
if (queryParams.size > 0) {
|
||||
const dbid = queryParams.get('dbid');
|
||||
const sql = queryParams.get('sql');
|
||||
const name = queryParams.get('name');
|
||||
const schema = queryParams.get('schema');
|
||||
const autorun = queryParams.get('autorun') === 'true';
|
||||
|
||||
const queryParamsState = {
|
||||
requestedQuery: { dbid, sql, name, schema, autorun },
|
||||
isDataset: true,
|
||||
} as LocationState;
|
||||
return <Provider value={queryParamsState}>{children}</Provider>;
|
||||
}
|
||||
|
||||
return <Provider value={EMPTY_STATE}>{children}</Provider>;
|
||||
};
|
||||
export const useLocationState = () => useContext(locationContext);
|
||||
|
||||
Reference in New Issue
Block a user