From 38339bb05220396f3dc63703940fe7638ae8bdbc Mon Sep 17 00:00:00 2001 From: cgu Date: Fri, 11 Dec 2020 19:18:10 -0500 Subject: [PATCH 1/2] Add force show functionality to meta info --- datahub/webapp/__tests__/lib/markdown.test.ts | 19 + .../DataDocStatementExecution.tsx | 169 ++++----- .../StatementMeta.tsx | 22 +- .../QueryExecution/QueryExecution.tsx | 345 ++++++++---------- datahub/webapp/hooks/useToggle.ts | 13 + docs/developer_guide/add_query_engine.md | 44 +++ 6 files changed, 310 insertions(+), 302 deletions(-) create mode 100644 datahub/webapp/hooks/useToggle.ts diff --git a/datahub/webapp/__tests__/lib/markdown.test.ts b/datahub/webapp/__tests__/lib/markdown.test.ts index fbaf6d3ba..c0e19a619 100644 --- a/datahub/webapp/__tests__/lib/markdown.test.ts +++ b/datahub/webapp/__tests__/lib/markdown.test.ts @@ -2,6 +2,9 @@ import { sanitizeAndExtraMarkdown } from 'lib/markdown'; describe('sanitizeAndExtraMarkdown', () => { it('Works for standard markdown', () => { + expect(sanitizeAndExtraMarkdown('')).toEqual(['', {}]); + expect(sanitizeAndExtraMarkdown('\n\n\n')).toEqual(['\n\n\n', {}]); + const markdown = 'Hello **World** ---\nHello There.'; expect(sanitizeAndExtraMarkdown(markdown)).toEqual([markdown, {}]); }); @@ -18,6 +21,22 @@ Hello **World**`; ]); }); + it('Extract Properties for multiple ---', () => { + const markdown = `test +--- +foo: bar +--- +test2 +--- +bar: baz +--- +Hello **World**`; + expect(sanitizeAndExtraMarkdown(markdown)).toEqual([ + 'test\ntest2\nHello **World**', + { foo: 'bar', bar: 'baz' }, + ]); + }); + it('Only extracts valid properties', () => { const markdown = `--- foobar diff --git a/datahub/webapp/components/DataDocStatementExecution/DataDocStatementExecution.tsx b/datahub/webapp/components/DataDocStatementExecution/DataDocStatementExecution.tsx index feaccf2c7..0791bda5a 100644 --- a/datahub/webapp/components/DataDocStatementExecution/DataDocStatementExecution.tsx +++ b/datahub/webapp/components/DataDocStatementExecution/DataDocStatementExecution.tsx @@ -1,8 +1,8 @@ -import { bind } from 'lodash-decorators'; -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { StatementExecutionStatus } from 'const/queryExecution'; +import { useToggle } from 'hooks/useToggle'; import { IStatementExecution, IStatementResult, @@ -16,73 +16,79 @@ import { Modal } from 'ui/Modal/Modal'; import { ProgressBar } from 'ui/ProgressBar/ProgressBar'; import './DataDocStatementExecution.scss'; +import { sanitizeAndExtraMarkdown } from 'lib/markdown'; interface IProps { statementExecution: IStatementExecution; statementResult: IStatementResult; - index: number; showStatementLogs: boolean; showStatementMeta: boolean; + toggleStatementMeta: () => any; loadS3Result: (id: number) => any; } -interface IState { - showInFullScreen: boolean; -} +function useStatementMeta( + metaInfo: string | null, + showStatementMeta: boolean, + toggleStatementMeta: () => void +) { + const [statementMeta, forceShowMeta] = useMemo(() => { + if (!metaInfo) { + return [null, false]; + } + const [processedMeta, metaProperties] = sanitizeAndExtraMarkdown( + metaInfo + ); + return [processedMeta, Boolean(metaProperties['force_show'])]; + }, [metaInfo]); -export class DataDocStatementExecution extends React.PureComponent< - IProps, - IState -> { - public constructor(props) { - super(props); - this.state = { - showInFullScreen: false, - }; - - this.loadStatementResult(this.props); - } - - @bind - public onFullscreenToggle() { - this.setState({ - showInFullScreen: true, - }); - } - - public componentDidUpdate(prevProps) { - if (this.props.statementExecution !== prevProps.statementExecution) { - this.loadStatementResult(this.props); + useEffect(() => { + if (forceShowMeta && !showStatementMeta) { + toggleStatementMeta(); } - } + }, [forceShowMeta]); + + return statementMeta; +} + +export const DataDocStatementExecution: React.FC = ({ + statementExecution, + statementResult, + + showStatementLogs, + showStatementMeta, + toggleStatementMeta, + + loadS3Result, +}) => { + const [showInFullScreen, setShowInFullScreen] = useState(false); + const statementMeta = useStatementMeta( + statementExecution.meta_info, + showStatementMeta, + toggleStatementMeta + ); - public loadStatementResult(props) { - const { statementExecution, statementResult, loadS3Result } = props; + const toggleFullScreen = useToggle(setShowInFullScreen); + useEffect(() => { if (statementExecution.result_row_count && !statementResult) { loadS3Result(statementExecution.id); } - } + }, [ + statementExecution.result_row_count, + statementExecution.id, + statementResult, + ]); - public makeLogDOM() { - return ( - - ); - } - - public makeMetaInfoDOM() { - return ( - - ); - } + const getLogDOM = () => ( + + ); - public makeContentDOM() { - const { statementExecution, statementResult } = this.props; + const getMetaInfoDOM = () => ; + const getContentDOM = () => { const { status } = statementExecution; let contentDOM = null; @@ -111,9 +117,9 @@ export class DataDocStatementExecution extends React.PureComponent<
Status: {statusLabel}
- {this.makeMetaInfoDOM()} + {getMetaInfoDOM()} {progressBar} - {this.makeLogDOM()} + {getLogDOM()} ); } else if (status === StatementExecutionStatus.UPLOADING) { @@ -124,24 +130,23 @@ export class DataDocStatementExecution extends React.PureComponent< Status: Uploading - {this.makeMetaInfoDOM()} + {getMetaInfoDOM()} Loading query results... - {this.makeLogDOM()} + {getLogDOM()} ); } else if (status === StatementExecutionStatus.DONE) { - const { showStatementLogs, showStatementMeta } = this.props; contentDOM = ( <> - {showStatementMeta && this.makeMetaInfoDOM()} - {showStatementLogs && this.makeLogDOM()} + {showStatementMeta && getMetaInfoDOM()} + {showStatementLogs && getLogDOM()} @@ -150,8 +155,8 @@ export class DataDocStatementExecution extends React.PureComponent< // error contentDOM = (
- {this.makeMetaInfoDOM()} - {this.makeLogDOM()} + {getMetaInfoDOM()} + {getLogDOM()}
); } else if (status === StatementExecutionStatus.CANCEL) { @@ -161,49 +166,31 @@ export class DataDocStatementExecution extends React.PureComponent<
Status: User Cancelled
- {this.makeMetaInfoDOM()} - {this.makeLogDOM()} + {getMetaInfoDOM()} + {getLogDOM()} ); } return
{contentDOM}
; - } - - public makeFullScreenModal() { - const { showInFullScreen } = this.state; + }; - const { statementExecution, statementResult } = this.props; - return showInFullScreen ? ( - this.setState({ showInFullScreen: false })} - > + const getFullScreenModal = () => + showInFullScreen ? ( + - this.setState({ showInFullScreen: false }) - } + onFullscreenToggle={toggleFullScreen} isFullscreen={true} /> ) : null; - } - - public render() { - const contentDOM = this.makeContentDOM(); - // const headerDOM = this.makeHeaderDOM(); - // const footerDOM = this.makeFooterDOM(); - - return ( - <> -
- {/* {headerDOM} */} - {contentDOM} -
- {this.makeFullScreenModal()} - - ); - } -} + + return ( + <> +
{getContentDOM()}
+ {getFullScreenModal()} + + ); +}; diff --git a/datahub/webapp/components/DataDocStatementExecution/StatementMeta.tsx b/datahub/webapp/components/DataDocStatementExecution/StatementMeta.tsx index d4958624e..2a324ba3b 100644 --- a/datahub/webapp/components/DataDocStatementExecution/StatementMeta.tsx +++ b/datahub/webapp/components/DataDocStatementExecution/StatementMeta.tsx @@ -1,22 +1,14 @@ import React from 'react'; import Markdown from 'markdown-to-jsx'; import { Message } from 'ui/Message/Message'; -import { linkifyLog } from 'lib/utils'; export const StatementMeta: React.FunctionComponent<{ metaInfo?: string }> = ({ metaInfo, -}) => { - const linkifiedMetaInfo = React.useMemo(() => linkifyLog(metaInfo ?? ''), [ - metaInfo, - ]); - - return ( - metaInfo && ( -
- - {linkifiedMetaInfo} - -
- ) +}) => + metaInfo && ( +
+ + {metaInfo} + +
); -}; diff --git a/datahub/webapp/components/QueryExecution/QueryExecution.tsx b/datahub/webapp/components/QueryExecution/QueryExecution.tsx index 7bd2223da..739bc2729 100644 --- a/datahub/webapp/components/QueryExecution/QueryExecution.tsx +++ b/datahub/webapp/components/QueryExecution/QueryExecution.tsx @@ -1,129 +1,160 @@ -import { bind } from 'lodash-decorators'; -import { decorate } from 'core-decorators'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { connect } from 'react-redux'; +import React, { useState, useCallback, useEffect, useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; -import { sendNotification } from 'lib/dataHubUI'; import { QueryExecutionStatus } from 'const/queryExecution'; -import { IQueryExecution } from 'redux/queryExecutions/types'; +import { useToggle } from 'hooks/useToggle'; +import { sendNotification } from 'lib/dataHubUI'; import * as queryExecutionsActions from 'redux/queryExecutions/action'; import { IStoreState, Dispatch } from 'redux/store/types'; +import { + queryExecutionSelector, + statementExecutionsSelector, +} from 'redux/queryExecutions/selector'; import { DataDocStatementExecutionBar } from 'components/DataDocStatementExecutionBar/DataDocStatementExecutionBar'; import { DataDocStatementExecution } from 'components/DataDocStatementExecution/DataDocStatementExecution'; import { StatementExecutionPicker } from 'components/ExecutionPicker/StatementExecutionPicker'; - import { Loader } from 'ui/Loader/Loader'; import { Loading } from 'ui/Loading/Loading'; import { ExecutedQueryCell } from './ExecutedQueryCell'; import { QueryErrorWrapper } from './QueryError'; import { QuerySteps } from './QuerySteps'; -import './QueryExecution.scss'; import { QueryExecutionFooter } from './QueryExecutionFooter'; -import { - queryExecutionSelector, - statementExecutionsSelector, -} from 'redux/queryExecutions/selector'; -import { queryEngineByIdEnvSelector } from 'redux/queryEngine/selector'; +import './QueryExecution.scss'; -interface IOwnProps { +interface IProps { id: number; docId?: number; changeCellContext?: (context: string) => any; } -type StateProps = ReturnType; -type DispatchProps = ReturnType; -type IProps = IOwnProps & StateProps & DispatchProps; -interface IState { - selectedStatementTabIndex: number; - showExecutedQuery: boolean; - showStatementLogs: boolean; - showStatementMeta: boolean; -} +function useQueryExecutionReduxState(queryId: number) { + // queryExecution: queryExecutionSelector(state, ownProps.id), + // statementExecutions: statementExecutionsSelector(state, ownProps.id), + // statementResultById: state.queryExecutions.statementResultById, + + const queryExecution = useSelector((state: IStoreState) => + queryExecutionSelector(state, queryId) + ); + const statementExecutions = useSelector((state: IStoreState) => + statementExecutionsSelector(state, queryId) + ); + const statementResultById = useSelector( + (state: IStoreState) => state.queryExecutions.statementResultById + ); -class QueryExecutionComponent extends React.Component { - public readonly state = { - selectedStatementTabIndex: 0, - showExecutedQuery: false, - showStatementLogs: false, - showStatementMeta: false, + return { + queryExecution, + statementExecutions, + statementResultById, }; +} - @decorate(memoizeOne) - public pollQueryExecution(queryExecution: IQueryExecution) { - if ( - queryExecution && - queryExecution.status <= QueryExecutionStatus.RUNNING - ) { - this.props.pollQueryExecution(queryExecution.id, this.props.docId); - } - } - - @bind - public selectStatementTabIndex(index: number) { - this.setState({ - selectedStatementTabIndex: index, - }); - } - - @bind - public selectStatementId(id: number) { - const { - queryExecution: { statement_executions: statementExecutionIds }, - } = this.props; - - this.selectStatementTabIndex( - Math.max(statementExecutionIds.indexOf(id), 0) - ); - } +function useQueryExecutionDispatch() { + const dispatch: Dispatch = useDispatch(); + const loadQueryExecutionIfNeeded = useCallback( + (queryExecutionId: number) => { + dispatch( + queryExecutionsActions.fetchQueryExecutionIfNeeded( + queryExecutionId + ) + ); + }, + [] + ); - @bind - public getStatementExecution() { - return this.props.statementExecutions[ - this.state.selectedStatementTabIndex - ]; - } + const pollQueryExecution = useCallback( + (queryExecutionId: number, docId?: number) => + dispatch( + queryExecutionsActions.pollQueryExecution( + queryExecutionId, + docId + ) + ), + [] + ); - @bind - public toggleShowExecutedQuery() { - this.setState({ - showExecutedQuery: !this.state.showExecutedQuery, - }); - } + const loadS3Result = useCallback( + (statementExecutionId: number) => + dispatch(queryExecutionsActions.fetchResult(statementExecutionId)), + [] + ); - @bind - public toggleShowStatementMeta() { - this.setState(({ showStatementMeta }) => ({ - showStatementMeta: !showStatementMeta, - })); - } + const cancelQueryExecution = useCallback( + (queryExecutionId: number) => + queryExecutionsActions + .cancelQueryExecution(queryExecutionId) + .then(() => { + sendNotification( + 'Cancelled! Please be patient as the cancellation takes some time.' + ); + }), + [] + ); + return { + loadQueryExecutionIfNeeded, + pollQueryExecution, + loadS3Result, + cancelQueryExecution, + }; +} - @bind - public toggleLogs() { - const showStatementLogs = !this.state.showStatementLogs; - this.setState({ showStatementLogs }); - } +export const QueryExecution: React.FC = ({ + id, + docId, + changeCellContext, +}) => { + const [statementIndex, setStatementIndex] = useState(0); + const [showExecutedQuery, setShowExecutedQuery] = useState(false); + const [showStatementLogs, setShowStatementLogs] = useState(false); + const [showStatementMeta, setShowStatementMeta] = useState(false); + + const { + queryExecution, + statementExecutions, + statementResultById, + } = useQueryExecutionReduxState(id); + + const statementExecution = useMemo( + () => statementExecutions?.[statementIndex], + [statementExecutions, statementIndex] + ); + + const { + loadQueryExecutionIfNeeded, + pollQueryExecution, + loadS3Result, + cancelQueryExecution, + } = useQueryExecutionDispatch(); + + const selectStatementId = useCallback( + (statementId: number) => { + const { + statement_executions: statementExecutionIds, + } = queryExecution; + setStatementIndex( + Math.max(statementExecutionIds.indexOf(statementId), 0) + ); + }, + [queryExecution] + ); - @bind - public renderQueryExecution() { - const { - queryExecution, - statementResultById, + const toggleShowExecutedQuery = useToggle(setShowExecutedQuery); + const toggleLogs = useToggle(setShowStatementLogs); + const toggleShowStatementMeta = useToggle(setShowStatementMeta); - loadS3Result, + useEffect(() => { + if ( + queryExecution && + queryExecution.status <= QueryExecutionStatus.RUNNING + ) { + pollQueryExecution(queryExecution.id, docId); + } + }, [queryExecution]); - changeCellContext, - } = this.props; - const { - selectedStatementTabIndex, - showExecutedQuery, - showStatementMeta, - } = this.state; + const getQueryExecutionDOM = () => { const { statement_executions: statementExecutionIds } = queryExecution; - const queryStepsDOM = ; if ( statementExecutionIds == null || @@ -131,8 +162,6 @@ class QueryExecutionComponent extends React.Component { ) { return
{queryStepsDOM}
; } - - const statementExecution = this.getStatementExecution(); const statementExecutionId = statementExecution ? statementExecution.id : null; @@ -143,8 +172,8 @@ class QueryExecutionComponent extends React.Component { statementResult={statementResultById[statementExecutionId]} showStatementMeta={showStatementMeta} loadS3Result={loadS3Result} - index={selectedStatementTabIndex} - showStatementLogs={this.state.showStatementLogs} + showStatementLogs={showStatementLogs} + toggleStatementMeta={toggleShowStatementMeta} /> ) : queryExecution.status <= QueryExecutionStatus.RUNNING ? ( @@ -163,14 +192,14 @@ class QueryExecutionComponent extends React.Component { /> ) : null; - const footerDOM = this.renderQueryExecutionFooter(); + const footerDOM = getQueryExecutionFooterDOM(); return (
{queryStepsDOM} - {this.renderQueryExecutionErrorDOM()} - {this.renderStatementExecutionHeader()} + {getQueryExecutionErrorDOM()} + {getStatementExecutionHeaderDOM()} {executedQueryDOM}
{statementExecutionDOM} @@ -180,33 +209,11 @@ class QueryExecutionComponent extends React.Component {
); - } - - public componentDidMount() { - this.pollQueryExecution(this.props.queryExecution); - } - - public componentDidUpdate() { - this.pollQueryExecution(this.props.queryExecution); - } - - public renderStatementExecutionHeader() { - const { - statementExecutions, - cancelQueryExecution, - queryExecution, - } = this.props; - - const { - showExecutedQuery, - showStatementLogs, - showStatementMeta, - } = this.state; + }; + const getStatementExecutionHeaderDOM = () => { const { id, status: queryStatus } = queryExecution; - const statementExecution = this.getStatementExecution(); - const statementExecutionBar = statementExecution ? ( { showExecutedQuery={showExecutedQuery} showStatementMeta={showStatementMeta} cancelQueryExecution={cancelQueryExecution.bind(null, id)} - toggleShowExecutedQuery={this.toggleShowExecutedQuery} - toggleLogs={this.toggleLogs} - toggleShowStatementMeta={this.toggleShowStatementMeta} + toggleShowExecutedQuery={toggleShowExecutedQuery} + toggleLogs={toggleLogs} + toggleShowStatementMeta={toggleShowStatementMeta} /> ) : null; @@ -227,7 +234,7 @@ class QueryExecutionComponent extends React.Component { statementExecution ? statementExecution.id : null } statementExecutions={statementExecutions} - onSelection={this.selectStatementId} + onSelection={selectStatementId} total={queryExecution.total} autoSelect /> @@ -243,11 +250,9 @@ class QueryExecutionComponent extends React.Component {
); - } - - public renderQueryExecutionErrorDOM() { - const { queryExecution, statementExecutions } = this.props; + }; + const getQueryExecutionErrorDOM = () => { if (queryExecution.status === QueryExecutionStatus.ERROR) { return ( { /> ); } - } - - public renderQueryExecutionFooter() { - const { queryExecution, statementExecutions } = this.props; + }; + const getQueryExecutionFooterDOM = () => { if (!queryExecution) { return; } @@ -271,64 +274,14 @@ class QueryExecutionComponent extends React.Component { statementExecutions={statementExecutions} /> ); - } - - public render() { - const { id, queryExecution, loadQueryExecutionIfNeeded } = this.props; - - return ( - - ); - } -} - -function mapStateToProps(state: IStoreState, ownProps: IOwnProps) { - return { - queryExecution: queryExecutionSelector(state, ownProps.id), - statementExecutions: statementExecutionsSelector(state, ownProps.id), - statementResultById: state.queryExecutions.statementResultById, }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - loadQueryExecutionIfNeeded: (queryExecutionId: number) => { - dispatch( - queryExecutionsActions.fetchQueryExecutionIfNeeded( - queryExecutionId - ) - ); - }, - - pollQueryExecution: (queryExecutionId: number, docId?: number) => - dispatch( - queryExecutionsActions.pollQueryExecution( - queryExecutionId, - docId - ) - ), - - loadS3Result: (statementExecutionId: number) => - dispatch(queryExecutionsActions.fetchResult(statementExecutionId)), - - cancelQueryExecution: (queryExecutionId: number) => { - queryExecutionsActions - .cancelQueryExecution(queryExecutionId) - .then(() => { - sendNotification( - 'Cancelled! Please be patient as the cancellation takes some time.' - ); - }); - }, - }; -} -export const QueryExecution = connect( - mapStateToProps, - mapDispatchToProps -)(QueryExecutionComponent); + return ( + + ); +}; diff --git a/datahub/webapp/hooks/useToggle.ts b/datahub/webapp/hooks/useToggle.ts new file mode 100644 index 000000000..b301ebdc3 --- /dev/null +++ b/datahub/webapp/hooks/useToggle.ts @@ -0,0 +1,13 @@ +import React, { useCallback } from 'react'; + +/** + * Create a toggle function for a boolean useState setter + * + * @param setter the setter function provided by useState + * @param deps the dependency array if the setter would be recreated + * during rerender + */ +export const useToggle = ( + setter: React.Dispatch>, + deps: any[] = [] +) => useCallback(() => setter((v) => !v), deps); diff --git a/docs/developer_guide/add_query_engine.md b/docs/developer_guide/add_query_engine.md index e875f890a..4ef1845bd 100644 --- a/docs/developer_guide/add_query_engine.md +++ b/docs/developer_guide/add_query_engine.md @@ -30,3 +30,47 @@ If you cannot include this engine as part of the open source project, you can al 1. Locate the plugin root directory for your customized Querybook, and find the folder called executor_plugin. 2. Add your engine code similiar to what's above. 3. Make sure it is included in the variable ALL_PLUGIN_EXECUTORS under executor_plugin/\_\_init\_\_.py + +## What can be customized + +Given most query engines can be used via Sqlalchemy connectors, the primary use case for creating your own query engine is to customize its behaviors to ensure it is secure and user friendly. In this section, we will go over some examples of customization done at Pinterest to see how a query engine can be configured. + +### Authentication + +The default query engines require a fixed SQL connection string (e.g. mysql://username:password@host:port/database) to authenticate to the query engine. However, you might want to do the following: + +- proxy user so that the query engine can perform another check on whether or not the user has access +- use user's auth token to connect to the query engine instead +- use service discovery such as zookeeper to load balance the host + +To add a query engine, you would need to add a client and an executor. When the Querybook initializes the client, it will always pass a parameter called `proxy_user` to the client to represent the user who requested the query. The proxy_user field is an integer that represents the Querybook database id of the Users table. You can use this to fetch the username of the user and pass it to the query engine. For example, the Presto implementation provided by Querybook would fetch the username and then write it to the 'Proxy-Authorization' header of the HTTP requests. If you want to pass additional tokens such as JWT, you would need to first customize the [authentication](add_auth.md) to store the JWT into the user.properties when the user logs in and then pass the `proxy_user`'s JWT to the query engine. + +To use service discovery, modify the `EXECUTOR_TEMPLATE` so that it accepts the zookeeper connection details. Alternatively, you can also modify how the connection string is parsed. Check out the Hive executor for an example. + +### Exceptions + +Querybook can be customized to give users more info when the query execution process throws an exception. There are 3 query execution categories currently in Querybook: + +```py +class QueryExecutionErrorType(Enum): + INTERNAL = 0 # Error came from python exception caused by celery worker + ENGINE = 1 # Error was thrown from the query engine + SYNTAX = 2 # Error is thrown from query engine and is caused by syntax +``` + +By default, all exceptions are categorized as `INTERNAL` because Querybook does not differentiate between exceptions thrown by Querybook or by the query engine. However, if the exceptions thrown by the query engine are all inherited from an error class, then you can override the `_parse_exception` to check if it is from the query engine. +A common case of query engine errors is the syntax error. Querybook provides a special UI for syntax error to show the user where exactly the syntax error is. If you want this integration for your own customized query engine, you should return by calling the function `get_parsed_syntax_error` with the line number and the starting character position. To see the actual code usage, check out the examples in executors/presto.py and hive.py. + +### Meta + +When a query is running, run time information can be extracted in the form of percentage completion, logs, and meta. This information gets streamed live via Websocket to the user. While the former two are specific, the meta form can contain free-formed markdown information that provides a summary to the query runner. As an example, when a user runs a Presto query, the meta field can contain the Presto tracking URL for them to check out more details about the query. The tracking URL to meta info behavior is provided by the base client and the base executor, which means the only implementation required by the developer is to generate the tracking URL in the inherited client. + +However, you may want to extend the meta_info function in the QueryExecutorBaseClass to provide other info to the user. For example, you can provide multiple URLs (e.g. log URL) or runtime query warnings to the user. By default, the meta field is only shown to the user while the query is running, and is collapsed by default. In case you do want to show the meta info to the user after a query has been completed, you can add the following text to the meta: + +``` +--- +force_show: true +--- +``` + +In the frontend, this part of the text will be parsed and removed from the meta. From c62c1568bdec366a20c1a2054338bda722e86375 Mon Sep 17 00:00:00 2001 From: cgu Date: Sat, 12 Dec 2020 11:36:05 -0500 Subject: [PATCH 2/2] fix linter test --- datahub/webapp/hooks/useToggle.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/datahub/webapp/hooks/useToggle.ts b/datahub/webapp/hooks/useToggle.ts index b301ebdc3..e3e149f45 100644 --- a/datahub/webapp/hooks/useToggle.ts +++ b/datahub/webapp/hooks/useToggle.ts @@ -4,8 +4,8 @@ import React, { useCallback } from 'react'; * Create a toggle function for a boolean useState setter * * @param setter the setter function provided by useState - * @param deps the dependency array if the setter would be recreated - * during rerender + * @param deps the dependency array if the setter would be recreated during rerender + * */ export const useToggle = ( setter: React.Dispatch>,