chore: migrate QueryTable component from jsx to tsx (#17944)

* chore: fix misspelling of button

* changed QuerySearch/index.jsx to index.tsx

* updated Query type at src/SqlLab/types.ts/Query and extended it into QueryTable

* updated defaultQueryLimit to be optional at ResultSetProps of superset-frontend/src/SqlLab/components/ResultSet/index.tsx

* moved statusAttributes into useMemo hook so that it will not make the dependencies of useMemo Hook change on every render and statusAttributes is only used within useMomo hook

* reset package-lock.json and package.json at frontend

* Acquired redux rootestate type for SqlLab, to be used at useSelector(state)

* updated query to be Query type and added a comment for q.status on line 178

* updated queryTable for typescript conversion

* updated actions type on QueryHistory and QuerySearch

* updated type for actions in SouthPane

* created type for SqlLab redux

Co-authored-by: David Woolner <davidwoolner@gmail.com>
Co-authored-by: AAfghahi <48933336+AAfghahi@users.noreply.github.com>
This commit is contained in:
Meitong Qu 2022-02-11 04:54:42 -08:00 committed by GitHub
parent 97d918b692
commit 9f678e55b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 202 additions and 120 deletions

View File

@ -24,7 +24,13 @@ import QueryTable from 'src/SqlLab/components/QueryTable';
interface QueryHistoryProps {
queries: Query[];
actions: Record<string, unknown>;
actions: {
queryEditorSetSql: Function;
cloneQueryToNewTab: Function;
fetchQueryResults: Function;
clearQueryResults: Function;
removeQuery: Function;
};
displayLimit: number;
}

View File

@ -37,6 +37,11 @@ interface QuerySearchProps {
actions: {
addDangerToast: (msg: string) => void;
setDatabases: (data: Record<string, any>) => Record<string, any>;
queryEditorSetSql: Function;
cloneQueryToNewTab: Function;
fetchQueryResults: Function;
clearQueryResults: Function;
removeQuery: Function;
};
displayLimit: number;
}

View File

@ -25,11 +25,13 @@ import TableView from 'src/components/TableView';
import TableCollection from 'src/components/TableCollection';
import { Provider } from 'react-redux';
import { queries, user } from 'src/SqlLab/fixtures';
import * as actions from 'src/SqlLab/actions/sqlLab';
describe('QueryTable', () => {
const mockedProps = {
queries,
displayLimit: 100,
actions,
};
it('is valid', () => {
expect(React.isValidElement(<QueryTable displayLimit={100} />)).toBe(true);

View File

@ -17,7 +17,6 @@
* under the License.
*/
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import Card from 'src/components/Card';
import ProgressBar from 'src/components/ProgressBar';
@ -29,133 +28,151 @@ import Button from 'src/components/Button';
import { fDuration } from 'src/modules/dates';
import Icons from 'src/components/Icons';
import { Tooltip } from 'src/components/Tooltip';
import { Query, RootState } from 'src/SqlLab/types';
import ModalTrigger from 'src/components/ModalTrigger';
import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
import ResultSet from '../ResultSet';
import ModalTrigger from '../../../components/ModalTrigger';
import HighlightedSql from '../HighlightedSql';
import { StaticPosition, verticalAlign, StyledTooltip } from './styles';
const propTypes = {
columns: PropTypes.array,
actions: PropTypes.object,
queries: PropTypes.array,
onUserClicked: PropTypes.func,
onDbClicked: PropTypes.func,
displayLimit: PropTypes.number.isRequired,
};
const defaultProps = {
columns: ['started', 'duration', 'rows'],
queries: [],
onUserClicked: () => {},
onDbClicked: () => {},
};
interface QueryTableQuery extends Omit<Query, 'state' | 'sql' | 'progress'> {
state?: Record<string, any>;
sql?: Record<string, any>;
progress?: Record<string, any>;
}
const openQuery = id => {
interface QueryTableProps {
columns?: string[];
actions: {
queryEditorSetSql: Function;
cloneQueryToNewTab: Function;
fetchQueryResults: Function;
clearQueryResults: Function;
removeQuery: Function;
};
queries?: Query[];
onUserClicked?: Function;
onDbClicked?: Function;
displayLimit: number;
}
const openQuery = (id: number) => {
const url = `/superset/sqllab?queryId=${id}`;
window.open(url);
};
const QueryTable = props => {
const QueryTable = ({
columns = ['started', 'duration', 'rows'],
actions,
queries = [],
onUserClicked = () => undefined,
onDbClicked = () => undefined,
displayLimit,
}: QueryTableProps) => {
const theme = useTheme();
const statusAttributes = {
success: {
config: {
icon: <Icons.Check iconColor={theme.colors.success.base} />,
label: t('Success'),
},
},
failed: {
config: {
icon: <Icons.XSmall iconColor={theme.colors.error.base} />,
label: t('Failed'),
},
},
stopped: {
config: {
icon: <Icons.XSmall iconColor={theme.colors.error.base} />,
label: t('Failed'),
},
},
running: {
config: {
icon: <Icons.Running iconColor={theme.colors.primary.base} />,
label: t('Running'),
},
},
fetching: {
config: {
icon: <Icons.Queued iconColor={theme.colors.primary.base} />,
label: t('fetching'),
},
},
timed_out: {
config: {
icon: <Icons.Offline iconColor={theme.colors.grayscale.light1} />,
label: t('Offline'),
},
},
scheduled: {
config: {
icon: <Icons.Queued iconColor={theme.colors.grayscale.base} />,
label: t('Scheduled'),
},
},
pending: {
config: {
icon: <Icons.Queued iconColor={theme.colors.grayscale.base} />,
label: t('Scheduled'),
},
},
error: {
config: {
icon: <Icons.Error iconColor={theme.colors.error.base} />,
label: t('Unknown Status'),
},
},
};
const setHeaders = column => {
const setHeaders = (column: string) => {
if (column === 'sql') {
return column.toUpperCase();
}
return column.charAt(0).toUpperCase().concat(column.slice(1));
};
const columns = useMemo(
const columnsOfTable = useMemo(
() =>
props.columns.map(column => ({
columns.map(column => ({
accessor: column,
Header: () => setHeaders(column),
disableSortBy: true,
})),
[props.columns],
[columns],
);
const user = useSelector(({ sqlLab: { user } }) => user);
const user = useSelector<RootState, User>(state => state.sqlLab.user);
const {
queryEditorSetSql,
cloneQueryToNewTab,
fetchQueryResults,
clearQueryResults,
removeQuery,
} = actions;
const data = useMemo(() => {
const restoreSql = query => {
props.actions.queryEditorSetSql({ id: query.sqlEditorId }, query.sql);
const restoreSql = (query: Query) => {
queryEditorSetSql({ id: query.sqlEditorId }, query.sql);
};
const openQueryInNewTab = query => {
props.actions.cloneQueryToNewTab(query, true);
const openQueryInNewTab = (query: Query) => {
cloneQueryToNewTab(query, true);
};
const openAsyncResults = (query, displayLimit) => {
props.actions.fetchQueryResults(query, displayLimit);
const openAsyncResults = (query: Query, displayLimit: number) => {
fetchQueryResults(query, displayLimit);
};
const clearQueryResults = query => {
props.actions.clearQueryResults(query);
const statusAttributes = {
success: {
config: {
icon: <Icons.Check iconColor={theme.colors.success.base} />,
label: t('Success'),
},
},
failed: {
config: {
icon: <Icons.XSmall iconColor={theme.colors.error.base} />,
label: t('Failed'),
},
},
stopped: {
config: {
icon: <Icons.XSmall iconColor={theme.colors.error.base} />,
label: t('Failed'),
},
},
running: {
config: {
icon: <Icons.Running iconColor={theme.colors.primary.base} />,
label: t('Running'),
},
},
fetching: {
config: {
icon: <Icons.Queued iconColor={theme.colors.primary.base} />,
label: t('Fetching'),
},
},
timed_out: {
config: {
icon: <Icons.Offline iconColor={theme.colors.grayscale.light1} />,
label: t('Offline'),
},
},
scheduled: {
config: {
icon: <Icons.Queued iconColor={theme.colors.grayscale.base} />,
label: t('Scheduled'),
},
},
pending: {
config: {
icon: <Icons.Queued iconColor={theme.colors.grayscale.base} />,
label: t('Scheduled'),
},
},
error: {
config: {
icon: <Icons.Error iconColor={theme.colors.error.base} />,
label: t('Unknown Status'),
},
},
};
const removeQuery = query => {
props.actions.removeQuery(query);
};
return props.queries
return queries
.map(query => {
const q = { ...query };
const status = statusAttributes[q.state] || statusAttributes.error;
const { state, sql, progress, ...rest } = query;
const q = rest as QueryTableQuery;
const status = statusAttributes[state] || statusAttributes.error;
if (q.endDttm) {
q.duration = fDuration(q.startDttm, q.endDttm);
@ -172,7 +189,7 @@ const QueryTable = props => {
<Button
buttonSize="small"
buttonStyle="link"
onClick={() => props.onUserClicked(q.userId)}
onClick={() => onUserClicked(q.userId)}
>
{q.user}
</Button>
@ -181,7 +198,7 @@ const QueryTable = props => {
<Button
buttonSize="small"
buttonStyle="link"
onClick={() => props.onDbClicked(q.dbId)}
onClick={() => onDbClicked(q.dbId)}
>
{q.db}
</Button>
@ -200,7 +217,7 @@ const QueryTable = props => {
q.sql = (
<Card css={[StaticPosition]}>
<HighlightedSql
sql={q.sql}
sql={sql}
rawSql={q.executedSql}
shrink
maxWidth={60}
@ -217,16 +234,17 @@ const QueryTable = props => {
</Label>
}
modalTitle={t('Data preview')}
beforeOpen={() => openAsyncResults(query, props.displayLimit)}
beforeOpen={() => openAsyncResults(query, displayLimit)}
onExit={() => clearQueryResults(query)}
modalBody={
<ResultSet
showSql
user={user}
query={query}
actions={props.actions}
actions={actions}
height={400}
displayLimit={props.displayLimit}
displayLimit={displayLimit}
defaultQueryLimit={1000}
/>
}
responsive
@ -240,17 +258,14 @@ const QueryTable = props => {
q.output = [schemaUsed, q.tempTable].filter(v => v).join('.');
}
q.progress =
q.state === 'success' ? (
state === 'success' ? (
<ProgressBar
percent={parseInt(q.progress.toFixed(0), 10)}
percent={parseInt(progress.toFixed(0), 10)}
striped
showInfo={false}
/>
) : (
<ProgressBar
percent={parseInt(q.progress.toFixed(0), 10)}
striped
/>
<ProgressBar percent={parseInt(progress.toFixed(0), 10)} striped />
);
q.state = (
<Tooltip title={status.config.label} placement="bottom">
@ -266,35 +281,44 @@ const QueryTable = props => {
)}
placement="top"
>
<Icons.Edit iconSize="small" />
<Icons.Edit iconSize="s" />
</StyledTooltip>
<StyledTooltip
onClick={() => openQueryInNewTab(query)}
tooltip={t('Run query in a new tab')}
placement="top"
>
<Icons.PlusCircleOutlined
iconSize="x-small"
css={verticalAlign}
/>
<Icons.PlusCircleOutlined iconSize="xs" css={verticalAlign} />
</StyledTooltip>
<StyledTooltip
tooltip={t('Remove query from log')}
onClick={() => removeQuery(query)}
>
<Icons.Trash iconSize="x-small" />
<Icons.Trash iconSize="xs" />
</StyledTooltip>
</div>
);
return q;
})
.reverse();
}, [props]);
}, [
queries,
onUserClicked,
onDbClicked,
user,
displayLimit,
actions,
clearQueryResults,
cloneQueryToNewTab,
fetchQueryResults,
queryEditorSetSql,
removeQuery,
]);
return (
<div className="QueryTable">
<TableView
columns={columns}
columns={columnsOfTable}
data={data}
className="table-condensed"
pageSize={50}
@ -303,7 +327,4 @@ const QueryTable = props => {
);
};
QueryTable.propTypes = propTypes;
QueryTable.defaultProps = defaultProps;
export default QueryTable;

View File

@ -44,7 +44,14 @@ interface SouthPanePropTypes {
editorQueries: any[];
latestQueryId?: string;
dataPreviewQueries: any[];
actions: Record<string, Function>;
actions: {
queryEditorSetSql: Function;
cloneQueryToNewTab: Function;
fetchQueryResults: Function;
clearQueryResults: Function;
removeQuery: Function;
setActiveSouthPaneTab: Function;
};
activeSouthPaneTab?: string;
height: number;
databases: Record<string, any>;

View File

@ -18,6 +18,8 @@
*/
import { SupersetError } from 'src/components/ErrorMessage/types';
import { CtasEnum } from 'src/SqlLab/actions/sqlLab';
import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes';
import { ToastType } from 'src/components/MessageToasts/types';
export type Column = {
name: string;
@ -68,6 +70,19 @@ export type Query = {
rows: number;
queryLimit: number;
limitingFactor: string;
endDttm: number;
duration: string;
startDttm: number;
time: Record<string, any>;
user: Record<string, any>;
userId: number;
db: Record<string, any>;
started: string;
querylink: Record<string, any>;
queryId: number;
executedSql: string;
output: string | Record<string, any>;
actions: Record<string, any>;
};
export interface QueryEditor {
@ -82,3 +97,29 @@ export interface QueryEditor {
errors: SupersetError[];
};
}
export type toastState = {
id: string;
toastType: ToastType;
text: string;
duration: number;
noDuplicate: boolean;
};
export type RootState = {
sqlLab: {
activeSouthPaneTab: string | number; // default is string; action.newQuery.id is number
alerts: any[];
databases: Record<string, any>;
offline: boolean;
queries: Query[];
queryEditors: QueryEditor[];
tabHistory: string[]; // default is activeTab ? [activeTab.id.toString()] : []
tables: Record<string, any>[];
queriesLastUpdate: number;
user: UserWithPermissionsAndRoles;
};
localStorageUsageInKilobytes: number;
messageToasts: toastState[];
common: {};
};

View File

@ -22,7 +22,7 @@ type AntdIconType = IconComponentProps;
type IconType = AntdIconType & {
iconColor?: string;
twoToneColor?: string;
iconSize?: 's' | 'm' | 'l' | 'xl' | 'xxl';
iconSize?: 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl';
};
export default IconType;