Skip to content

Commit

Permalink
feat: SQL preview modal for Query History (apache#11634)
Browse files Browse the repository at this point in the history
  • Loading branch information
nytai authored Nov 21, 2020
1 parent a3a2a68 commit fbe4a66
Show file tree
Hide file tree
Showing 14 changed files with 703 additions and 108 deletions.
5 changes: 4 additions & 1 deletion superset-frontend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ module.exports = {
'^spec/(.*)$': '<rootDir>/spec/$1',
},
testEnvironment: 'enzyme',
setupFilesAfterEnv: ['jest-enzyme', '<rootDir>/spec/helpers/shim.ts'],
setupFilesAfterEnv: [
'<rootDir>/node_modules/jest-enzyme/lib/index.js',
'<rootDir>/spec/helpers/shim.ts',
],
testURL: 'http://localhost',
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'],
coverageDirectory: '<rootDir>/coverage/',
Expand Down
1 change: 1 addition & 0 deletions superset-frontend/src/common/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { DropDownProps } from 'antd/lib/dropdown';
// eslint-disable-next-line no-restricted-imports
export {
Avatar,
Button,
Card,
Collapse,
DatePicker,
Expand Down
7 changes: 7 additions & 0 deletions superset-frontend/src/messageToasts/enhancers/withToasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ import {
addWarningToast,
} from '../actions';

export interface ToastProps {
addDangerToast: typeof addDangerToast;
addInfoToast: typeof addInfoToast;
addSuccessToast: typeof addSuccessToast;
addWarningToast: typeof addWarningToast;
}

// To work properly the redux state must have a `messageToasts` subtree
export default function withToasts(BaseComponent: ComponentType<any>) {
return connect(null, dispatch =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* 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 React from 'react';
import { styled, t } from '@superset-ui/core';
import { SyntaxHighlighterProps } from 'react-syntax-highlighter';
import sqlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
import htmlSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/htmlbars';
import markdownSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/markdown';
import jsonSyntax from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';
import { ToastProps } from 'src/messageToasts/enhancers/withToasts';
import Icon from 'src/components/Icon';

SyntaxHighlighter.registerLanguage('sql', sqlSyntax);
SyntaxHighlighter.registerLanguage('markdown', markdownSyntax);
SyntaxHighlighter.registerLanguage('html', htmlSyntax);
SyntaxHighlighter.registerLanguage('json', jsonSyntax);

const SyntaxHighlighterWrapper = styled.div`
margin-top: -24px;
&:hover {
svg {
visibility: visible;
}
}
svg {
position: relative;
top: 40px;
left: 512px;
visibility: hidden;
margin: -4px;
}
`;

export default function SyntaxHighlighterCopy({
addDangerToast,
addSuccessToast,
children,
...syntaxHighlighterProps
}: SyntaxHighlighterProps & {
children: string;
addDangerToast?: ToastProps['addDangerToast'];
addSuccessToast?: ToastProps['addSuccessToast'];
language: 'sql' | 'markdown' | 'html' | 'json';
}) {
function copyToClipboard(textToCopy: string) {
const selection: Selection | null = document.getSelection();
if (selection) {
selection.removeAllRanges();
const range = document.createRange();
const span = document.createElement('span');
span.textContent = textToCopy;
span.style.position = 'fixed';
span.style.top = '0';
span.style.clip = 'rect(0, 0, 0, 0)';
span.style.whiteSpace = 'pre';

document.body.appendChild(span);
range.selectNode(span);
selection.addRange(range);

try {
if (!document.execCommand('copy')) {
throw new Error(t('Not successful'));
}
} catch (err) {
if (addDangerToast) {
addDangerToast(t('Sorry, your browser does not support copying.'));
}
}

document.body.removeChild(span);
if (selection.removeRange) {
selection.removeRange(range);
} else {
selection.removeAllRanges();
}
if (addSuccessToast) {
addSuccessToast(t('SQL Copied!'));
}
}
}
return (
<SyntaxHighlighterWrapper>
<Icon
tabIndex={0}
name="copy"
role="button"
onClick={e => {
e.preventDefault();
e.currentTarget.blur();
copyToClipboard(children);
}}
/>
<SyntaxHighlighter style={github} {...syntaxHighlighterProps}>
{children}
</SyntaxHighlighter>
</SyntaxHighlighterWrapper>
);
}
75 changes: 75 additions & 0 deletions superset-frontend/src/views/CRUD/data/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* 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 } from 'react';

type BaseQueryObject = {
id: number;
};
export function useQueryPreviewState<D extends BaseQueryObject = any>({
queries,
fetchData,
currentQueryId,
}: {
queries: D[];
fetchData: (id: number) => any;
currentQueryId: number;
}) {
const index = queries.findIndex(query => query.id === currentQueryId);
const [currentIndex, setCurrentIndex] = useState(index);
const [disablePrevious, setDisablePrevious] = useState(false);
const [disableNext, setDisableNext] = useState(false);

function checkIndex() {
setDisablePrevious(currentIndex === 0);
setDisableNext(currentIndex === queries.length - 1);
}

function handleDataChange(previous: boolean) {
const offset = previous ? -1 : 1;
const index = currentIndex + offset;
if (index >= 0 && index < queries.length) {
fetchData(queries[index].id);
setCurrentIndex(index);
checkIndex();
}
}

function handleKeyPress(ev: any) {
if (currentIndex >= 0 && currentIndex < queries.length) {
if (ev.key === 'ArrowDown' || ev.key === 'k') {
ev.preventDefault();
handleDataChange(false);
} else if (ev.key === 'ArrowUp' || ev.key === 'j') {
ev.preventDefault();
handleDataChange(true);
}
}
}

useEffect(() => {
checkIndex();
});

return {
handleKeyPress,
handleDataChange,
disablePrevious,
disableNext,
};
}
19 changes: 18 additions & 1 deletion superset-frontend/src/views/CRUD/data/query/QueryList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ import React from 'react';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import fetchMock from 'fetch-mock';
import { act } from 'react-dom/test-utils';

import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
import { styledMount as mount } from 'spec/helpers/theming';

import QueryList, { QueryObject } from 'src/views/CRUD/data/query/QueryList';
import QueryList from 'src/views/CRUD/data/query/QueryList';
import QueryPreviewModal from 'src/views/CRUD/data/query/QueryPreviewModal';
import { QueryObject } from 'src/views/CRUD/types';
import ListView from 'src/components/ListView';
import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';

Expand All @@ -43,6 +46,7 @@ const mockQueries: QueryObject[] = [...new Array(3)].map((_, i) => ({
},
schema: 'public',
sql: `SELECT ${i} FROM table`,
executed_sql: `SELECT ${i} FROM table`,
sql_tables: [
{ schema: 'foo', table: 'table' },
{ schema: 'bar', table: 'table_2' },
Expand Down Expand Up @@ -97,4 +101,17 @@ describe('QueryList', () => {
it('renders a SyntaxHighlight', () => {
expect(wrapper.find(SyntaxHighlighter)).toExist();
});

it('opens a query preview', () => {
act(() => {
const props = wrapper
.find('[data-test="open-sql-preview-0"]')
.first()
.props();
if (props.onClick) props.onClick({} as React.MouseEvent);
});
wrapper.update();

expect(wrapper.find(QueryPreviewModal)).toExist();
});
});
Loading

0 comments on commit fbe4a66

Please sign in to comment.