diff --git a/datahub/webapp/__tests__/lib/data-doc/search.test.ts b/datahub/webapp/__tests__/lib/data-doc/search.test.ts new file mode 100644 index 000000000..ca41ad50d --- /dev/null +++ b/datahub/webapp/__tests__/lib/data-doc/search.test.ts @@ -0,0 +1,38 @@ +import { replaceStringIndices } from 'lib/data-doc/search'; + +const testString = 'the quick brown fox jumps over the lazy dog'; + +test('Replace empty case', () => { + expect(replaceStringIndices(testString, [], 'ze')).toBe( + 'the quick brown fox jumps over the lazy dog' + ); +}); + +test('Replace simple case', () => { + expect(replaceStringIndices(testString, [[0, 3]], 'ze')).toBe( + 'ze quick brown fox jumps over the lazy dog' + ); +}); + +test('Replace multiple case', () => { + expect( + replaceStringIndices( + testString, + [ + [0, 3], + [31, 34], + ], + 'le' + ) + ).toBe('le quick brown fox jumps over le lazy dog'); + expect( + replaceStringIndices( + testString, + [ + [16, 19], + [40, 43], + ], + 'tiger' + ) + ).toBe('the quick brown tiger jumps over the lazy tiger'); +}); diff --git a/datahub/webapp/__tests__/lib/utils/index.test.ts b/datahub/webapp/__tests__/lib/utils/index.test.ts index 267c325bc..2cb2f9039 100644 --- a/datahub/webapp/__tests__/lib/utils/index.test.ts +++ b/datahub/webapp/__tests__/lib/utils/index.test.ts @@ -1,6 +1,6 @@ import * as utils from 'lib/utils/index'; -// missing getSelectionRect, download, copy, scrollToElement, smoothScroll +// missing getSelectionRect, download, copy, smoothScroll test('removeEmpty', () => { expect(utils.removeEmpty({ notempty: 'test', empty: null })).toStrictEqual({ @@ -18,7 +18,7 @@ test('titleize', () => { test('sleep', () => { const mockFunction = jest.fn(() => { - console.log('mock function runs'); + // console.log('mock function runs'); }); const testFunction = async () => { diff --git a/datahub/webapp/components/DataDoc/DataDoc.scss b/datahub/webapp/components/DataDoc/DataDoc.scss index ba705f633..d51e8a77c 100644 --- a/datahub/webapp/components/DataDoc/DataDoc.scss +++ b/datahub/webapp/components/DataDoc/DataDoc.scss @@ -207,4 +207,10 @@ } } } + + .SearchAndReplaceBar { + position: fixed; + right: 60px; + z-index: 10; + } } diff --git a/datahub/webapp/components/DataDoc/DataDoc.tsx b/datahub/webapp/components/DataDoc/DataDoc.tsx index fa372abe4..ebbfaedf9 100644 --- a/datahub/webapp/components/DataDoc/DataDoc.tsx +++ b/datahub/webapp/components/DataDoc/DataDoc.tsx @@ -1,14 +1,19 @@ import React from 'react'; import { connect } from 'react-redux'; import { ContentState } from 'draft-js'; -import { findIndex, sample } from 'lodash'; +import { findIndex } from 'lodash'; import { bind, debounce } from 'lodash-decorators'; import { decorate } from 'core-decorators'; import memoizeOne from 'memoize-one'; import classNames from 'classnames'; -import { CELL_TYPE, IDataDoc, IDataCell } from 'const/datadoc'; +import { + CELL_TYPE, + IDataDoc, + IDataCell, + DataCellUpdateFields, +} from 'const/datadoc'; import ds from 'lib/datasource'; import history from 'lib/router-history'; import { sendConfirm, sendNotification } from 'lib/dataHubUI'; @@ -34,7 +39,6 @@ import { DataDocRightSidebar } from 'components/DataDocRightSidebar/DataDocRight import { DataDocUIGuide } from 'components/UIGuide/DataDocUIGuide'; import { DataDocCell } from 'components/DataDocCell/DataDocCell'; -import { Title } from 'ui/Title/Title'; import { Message } from 'ui/Message/Message'; import { Loading } from 'ui/Loading/Loading'; @@ -44,8 +48,14 @@ import { DataDocError } from './DataDocError'; import { DataDocContentContainer } from './DataDocContentContainer'; import './DataDoc.scss'; +import { DataDocLoading } from './DataDocLoading'; -const loadingHints: string[] = require('config/loading_hints.yaml').hints; +import { searchDataDoc, replaceDataDoc } from 'lib/data-doc/search'; +import { + ISearchAndReplaceHandles, + SearchAndReplace, +} from 'components/SearchAndReplace/SearchAndReplace'; +import { ISearchOptions, ISearchResult } from 'const/searchAndReplace'; interface IOwnProps { docId: number; @@ -63,6 +73,8 @@ interface IState { defaultCollapseAllCells: boolean; cellIdToExecutionId: Record; highlightCellIndex?: number; + + showSearchAndReplace: boolean; } class DataDocComponent extends React.Component { @@ -74,8 +86,13 @@ class DataDocComponent extends React.Component { connected: false, defaultCollapseAllCells: null, cellIdToExecutionId: {}, + + showSearchAndReplace: false, }; + // + public searchAndReplaceRef = React.createRef(); + public componentDidMount() { this.autoFocusCell({}, this.props); this.openDataDoc(this.props.docId); @@ -95,14 +112,38 @@ class DataDocComponent extends React.Component { this.setState({ defaultCollapseAllCells: null, focusedCellIndex: null, + + // Sharing State cellIdToExecutionId: {}, highlightCellIndex: null, }); + // Reset search + this.searchAndReplaceRef.current?.reset(); } this.openDataDoc(this.props.docId); } - if (this.props.dataDoc !== prevProps.dataDoc && this.props.dataDoc) { + if ( + this.props.dataDoc?.dataDocCells !== prevProps.dataDoc?.dataDocCells + ) { + const cells = this.props.dataDoc?.dataDocCells ?? []; + const previousCells = prevProps.dataDoc?.dataDocCells ?? []; + const someCellsContextChanged = + cells.length !== previousCells.length || + cells.some( + (cell, index) => + cell.context !== previousCells[index].context + ); + + if (someCellsContextChanged) { + this.searchAndReplaceRef.current?.performSearch(); + } + } + + if ( + this.props.dataDoc?.title !== prevProps.dataDoc?.title && + this.props.dataDoc?.title + ) { this.publishDataDocTitle(this.props.dataDoc.title); } } @@ -209,19 +250,21 @@ class DataDocComponent extends React.Component { // This is to quickly snap to the element, and then in case // if the cell above/below pushes the element out of view we // try to scroll it back - scrollToCell(dataDoc.dataDocCells[cellIndex], 0).then( - () => - this.setState( - { - highlightCellIndex: cellIndex, - }, - () => - scrollToCell( - dataDoc.dataDocCells[cellIndex], - 200, - 5 - ) - ) + scrollToCell( + dataDoc.dataDocCells[cellIndex].id, + 0 + ).then(() => + this.setState( + { + highlightCellIndex: cellIndex, + }, + () => + scrollToCell( + dataDoc.dataDocCells[cellIndex].id, + 200, + 5 + ) + ) ); } } @@ -244,6 +287,35 @@ class DataDocComponent extends React.Component { } } + @bind + public getSearchResults( + searchString: string, + searchOptions: ISearchOptions + ) { + return searchDataDoc(this.props.dataDoc, searchString, searchOptions); + } + + @bind + public replace( + searchResultsToReplace: ISearchResult[], + replaceString: string + ) { + return replaceDataDoc( + this.props.dataDoc, + searchResultsToReplace, + replaceString, + (cellId, context) => this.updateCell(cellId, { context }) + ); + } + + @bind + public async jumpToResult(result: ISearchResult) { + const cellId = result?.cellId; + if (cellId != null) { + await scrollToCell(cellId, 0); + } + } + @bind public async insertCellAt( index: number, @@ -268,6 +340,11 @@ class DataDocComponent extends React.Component { } } + @bind + public updateCell(cellId: number, fields: DataCellUpdateFields) { + return this.props.updateDataDocCell(this.props.docId, cellId, fields); + } + @bind public handleToggleCollapse() { this.setState(({ defaultCollapseAllCells }) => ({ @@ -320,6 +397,7 @@ class DataDocComponent extends React.Component { }, insertCellAt: this.insertCellAt, + updateCell: this.updateCell, defaultCollapse, focusedCellIndex, @@ -388,28 +466,6 @@ class DataDocComponent extends React.Component { ds.save(`/datadoc/${this.props.docId}/run/`); } - public makeDataDocLoadingDOM() { - // Get a random hint from list of hints - const hint = sample(loadingHints); - - return ( -
-
- - <i className="fa fa-spinner fa-pulse" /> -   Loading DataDoc - - -
-

- -   Did you know: {hint} -

-
-
- ); - } - @bind public renderLazyDataDocCell( cell: IDataCell, @@ -499,7 +555,12 @@ class DataDocComponent extends React.Component { changeDataDocMeta, } = this.props; - const { connected, defaultCollapseAllCells } = this.state; + const { + connected, + defaultCollapseAllCells, + + showSearchAndReplace, + } = this.state; let docDOM = null; let isSavingDataDoc = false; @@ -540,7 +601,7 @@ class DataDocComponent extends React.Component { ); } else { - docDOM = this.makeDataDocLoadingDOM(); + docDOM = ; } const leftSideBar = ( @@ -571,9 +632,16 @@ class DataDocComponent extends React.Component { key="data-hub-data-doc" > - {leftSideBar} - {docDOM} - {rightSideBar} + + {leftSideBar} + {docDOM} + {rightSideBar} + ); @@ -659,6 +727,25 @@ function mapDispatchToProps(dispatch: Dispatch, ownProps: IOwnProps) { meta ) ), + + updateDataDocCell: ( + docId: number, + cellId: number, + fields: DataCellUpdateFields + ) => { + try { + return dispatch( + dataDocActions.updateDataDocCell( + docId, + cellId, + fields.context, + fields.meta + ) + ); + } catch (e) { + sendNotification(`Cannot update cell, reason: ${e}`); + } + }, }; } diff --git a/datahub/webapp/components/DataDoc/DataDocLoading.tsx b/datahub/webapp/components/DataDoc/DataDocLoading.tsx new file mode 100644 index 000000000..1bb92484a --- /dev/null +++ b/datahub/webapp/components/DataDoc/DataDocLoading.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { sample } from 'lodash'; +import { Title } from 'ui/Title/Title'; + +const loadingHints: string[] = require('config/loading_hints.yaml').hints; + +export const DataDocLoading: React.FC = () => { + const hint = sample(loadingHints); + + return ( +
+
+ + <i className="fa fa-spinner fa-pulse" /> +   Loading DataDoc + + +
+

+ +   Did you know: {hint} +

+
+
+ ); +}; diff --git a/datahub/webapp/components/DataDocCell/DataDocCell.tsx b/datahub/webapp/components/DataDocCell/DataDocCell.tsx index 7ed0b1592..650aaf4be 100644 --- a/datahub/webapp/components/DataDocCell/DataDocCell.tsx +++ b/datahub/webapp/components/DataDocCell/DataDocCell.tsx @@ -4,7 +4,7 @@ import { ContentState } from 'draft-js'; import { useSelector, useDispatch } from 'react-redux'; import { IStoreState, Dispatch } from 'redux/store/types'; -import { IDataCell, IDataDoc } from 'const/datadoc'; +import { IDataCell, IDataDoc, DataCellUpdateFields } from 'const/datadoc'; import { DataDocContext } from 'context/DataDoc'; import { sendNotification, sendConfirm } from 'lib/dataHubUI'; @@ -53,6 +53,8 @@ export const DataDocCell: React.FunctionComponent = ({ const { cellIdToExecutionId, insertCellAt, + updateCell, + cellFocus, defaultCollapse, focusedCellIndex, @@ -92,21 +94,8 @@ export const DataDocCell: React.FunctionComponent = ({ const uncollapseCell = () => setShowCollapsed(false); const updateCellAt = React.useCallback( - async (fields: { context?: string | ContentState; meta?: {} }) => { - try { - await dispatch( - dataDocActions.updateDataDocCell( - dataDoc.id, - cell.id, - fields.context, - fields.meta - ) - ); - } catch (e) { - sendNotification(`Cannot update cell, reason: ${e}`); - } - }, - [cell.id, dataDoc.id] + async (fields: DataCellUpdateFields) => updateCell(cell.id, fields), + [cell.id] ); const handleDefaultCollapseChange = React.useCallback( @@ -214,7 +203,13 @@ export const DataDocCell: React.FunctionComponent = ({ ); } else if (cell.cell_type === 'text') { // default text - cellDOM = ; + cellDOM = ( + + ); } return cellDOM; diff --git a/datahub/webapp/components/DataDocLeftSidebar/DataDocContents.tsx b/datahub/webapp/components/DataDocLeftSidebar/DataDocContents.tsx index 91046c73d..7971c77a7 100644 --- a/datahub/webapp/components/DataDocLeftSidebar/DataDocContents.tsx +++ b/datahub/webapp/components/DataDocLeftSidebar/DataDocContents.tsx @@ -64,7 +64,7 @@ export const DataDocContents: React.FunctionComponent<{
  • scrollToCell(cell)} + onClick={() => scrollToCell(cell.id)} > {cellIcon} {cellText} diff --git a/datahub/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx b/datahub/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx index d475ba0ee..a50952745 100644 --- a/datahub/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx +++ b/datahub/webapp/components/DataDocQueryCell/DataDocQueryCell.tsx @@ -10,8 +10,6 @@ import { ICodeAnalysis, getSelectedQuery } from 'lib/sql-helper/sql-lexer'; import { renderTemplatedQuery } from 'lib/templated-query'; import { sleep, getCodeEditorTheme } from 'lib/utils'; import { sendNotification } from 'lib/dataHubUI'; -import { formatError } from 'lib/utils/error'; - import { IDataQueryCellMeta } from 'const/datadoc'; import * as dataSourcesActions from 'redux/dataSources/action'; @@ -26,18 +24,22 @@ import { IStoreState, Dispatch } from 'redux/store/types'; import { DataDocQueryExecutions } from 'components/DataDocQueryExecutions/DataDocQueryExecutions'; import { QueryEditor } from 'components/QueryEditor/QueryEditor'; import { QuerySnippetInsertionModal } from 'components/QuerySnippetInsertionModal/QuerySnippetInsertionModal'; -import { QueryRunButton } from 'components/QueryRunButton/QueryRunButton'; +import { + QueryRunButton, + IQueryRunButtonHandles, +} from 'components/QueryRunButton/QueryRunButton'; +import { CodeMirrorSearchHighlighter } from 'components/SearchAndReplace/CodeMirrorSearchHighlighter'; import { DebouncedInput } from 'ui/DebouncedInput/DebouncedInput'; import { DropdownMenu, IMenuItem } from 'ui/DropdownMenu/DropdownMenu'; import { Title } from 'ui/Title/Title'; import { Modal } from 'ui/Modal/Modal'; - -import './DataDocQueryCell.scss'; import { Message } from 'ui/Message/Message'; import { Button } from 'ui/Button/Button'; import { Icon } from 'ui/Icon/Icon'; +import './DataDocQueryCell.scss'; + const ON_CHANGE_DEBOUNCE_MS = 250; type StateProps = ReturnType; @@ -91,7 +93,7 @@ interface IState { class DataDocQueryCellComponent extends React.Component { private queryEditorRef = React.createRef(); - private runButtonRef = React.createRef(); + private runButtonRef = React.createRef(); private selfRef = React.createRef(); private keyMap = { 'Shift-Enter': this.clickOnRunButton, @@ -588,6 +590,10 @@ class DataDocQueryCellComponent extends React.Component { metastoreId={queryEngine.metastore_id} showFullScreenButton /> + ); diff --git a/datahub/webapp/components/DataDocTextCell/DataDocTextCell.tsx b/datahub/webapp/components/DataDocTextCell/DataDocTextCell.tsx index 8ded3dd71..8cd00bca4 100644 --- a/datahub/webapp/components/DataDocTextCell/DataDocTextCell.tsx +++ b/datahub/webapp/components/DataDocTextCell/DataDocTextCell.tsx @@ -3,12 +3,14 @@ import { debounce, bind } from 'lodash-decorators'; import React from 'react'; import classNames from 'classnames'; +import { DraftJsSearchHighlighter } from 'components/SearchAndReplace/DraftJsSearchHighlighter'; import { RichTextEditor } from 'ui/RichTextEditor/RichTextEditor'; import './DataDocTextCell.scss'; interface IProps { - context: DraftJs.ContentState; + cellId: number; + context: DraftJs.ContentState; meta: {}; isEditable: boolean; @@ -19,7 +21,6 @@ interface IProps { meta?: {}; }) => any; onDeleteKeyPressed?: () => any; - onFocus?: () => any; onBlur?: () => any; onUpKeyPressed?: () => any; @@ -183,6 +184,10 @@ export class DataDocTextCell extends React.Component { onChange={this.handleChange} readOnly={!this.props.isEditable} /> + ); } diff --git a/datahub/webapp/components/QueryComposer/QueryComposer.scss b/datahub/webapp/components/QueryComposer/QueryComposer.scss index 1cc85c2a6..4e3cb1663 100644 --- a/datahub/webapp/components/QueryComposer/QueryComposer.scss +++ b/datahub/webapp/components/QueryComposer/QueryComposer.scss @@ -35,6 +35,13 @@ .query-editor-wrapper { flex: 2; overflow: auto; + position: relative; + + .SearchAndReplaceBar { + position: absolute; + z-index: 9; + right: 40px; + } } .query-execution-wrapper { diff --git a/datahub/webapp/components/QueryComposer/QueryComposer.tsx b/datahub/webapp/components/QueryComposer/QueryComposer.tsx index 89bd32354..7523751ec 100644 --- a/datahub/webapp/components/QueryComposer/QueryComposer.tsx +++ b/datahub/webapp/components/QueryComposer/QueryComposer.tsx @@ -1,7 +1,14 @@ -import React, { useRef, useCallback, useMemo } from 'react'; +import React, { + useRef, + useCallback, + useMemo, + useEffect, + useState, +} from 'react'; import Resizable from 're-resizable'; import { useSelector, useDispatch } from 'react-redux'; +import { ISearchOptions, ISearchResult } from 'const/searchAndReplace'; import { getCodeEditorTheme, getQueryEngineId, @@ -19,7 +26,10 @@ import * as queryExecutionsAction from 'redux/queryExecutions/action'; import * as dataSourcesActions from 'redux/dataSources/action'; import * as adhocQueryActions from 'redux/adhocQuery/action'; -import { QueryRunButton } from 'components/QueryRunButton/QueryRunButton'; +import { + QueryRunButton, + IQueryRunButtonHandles, +} from 'components/QueryRunButton/QueryRunButton'; import { QueryEditor } from 'components/QueryEditor/QueryEditor'; import { FullHeight } from 'ui/FullHeight/FullHeight'; import { IconButton } from 'ui/Button/IconButton'; @@ -29,6 +39,13 @@ import { Button } from 'ui/Button/Button'; import './QueryComposer.scss'; import { QueryComposerExecution } from './QueryComposerExecution'; import { useDebounceState } from 'hooks/redux/useDebounceState'; +import { searchText, replaceStringIndices } from 'lib/data-doc/search'; +import { CodeMirrorSearchHighlighter } from 'components/SearchAndReplace/CodeMirrorSearchHighlighter'; +import { + SearchAndReplace, + ISearchAndReplaceHandles, + ISearchAndReplaceProps, +} from 'components/SearchAndReplace/SearchAndReplace'; const useExecution = (dispatch: Dispatch) => { const executionId = useSelector( @@ -86,6 +103,44 @@ const useQuery = (dispatch: Dispatch) => { return { query, setQuery }; }; +const useQueryComposerSearchAndReplace = ( + query: string, + setQuery: (s: string) => any, + searchAndReplaceRef: React.MutableRefObject +): ISearchAndReplaceProps => { + const getSearchResults = useCallback( + (searchString: string, searchOptions: ISearchOptions) => + searchText(query, searchString, searchOptions), + [query] + ); + const replace = useCallback( + (searchResultsToReplace: ISearchResult[], replaceString: string) => { + setQuery( + replaceStringIndices( + query, + searchResultsToReplace.map((r) => [r.from, r.to]), + replaceString + ) + ); + }, + [query] + ); + const jumpToResult = useCallback( + (ignore: ISearchResult) => Promise.resolve(), + [] + ); + + useEffect(() => { + searchAndReplaceRef.current?.performSearch(); + }, [query]); + + return { + getSearchResults, + replace, + jumpToResult, + }; +}; + export const QueryComposer: React.FC<{}> = () => { const dispatch: Dispatch = useDispatch(); const { query, setQuery } = useQuery(dispatch); @@ -97,7 +152,8 @@ export const QueryComposer: React.FC<{}> = () => { getCodeEditorTheme(state.user.computedSettings['theme']) ); const queryEditorRef = useRef(null); - const runButtonRef = useRef(null); + const runButtonRef = useRef(null); + const searchAndReplaceRef = useRef(null); const clickOnRunButton = useCallback(() => { if (runButtonRef.current) { @@ -122,6 +178,12 @@ export const QueryComposer: React.FC<{}> = () => { [engine] ); + const searchAndReplaceProps = useQueryComposerSearchAndReplace( + query, + setQuery, + searchAndReplaceRef + ); + const handleRunQuery = React.useCallback(async () => { // Just to throttle to prevent double running await sleep(250); @@ -145,18 +207,24 @@ export const QueryComposer: React.FC<{}> = () => { ); const editorDOM = ( - + <> + + + ); const executionDOM = executionId != null && ( @@ -179,7 +247,14 @@ export const QueryComposer: React.FC<{}> = () => { const contentDOM = (
    -
    {editorDOM}
    +
    + + {editorDOM} + +
    {executionDOM}
    ); diff --git a/datahub/webapp/components/QueryEditor/QueryEditor.tsx b/datahub/webapp/components/QueryEditor/QueryEditor.tsx index f9a647b37..36312e9d1 100644 --- a/datahub/webapp/components/QueryEditor/QueryEditor.tsx +++ b/datahub/webapp/components/QueryEditor/QueryEditor.tsx @@ -238,6 +238,7 @@ export class QueryEditor extends React.PureComponent { theme, matchBrackets: true, autoCloseBrackets: true, + highlightSelectionMatches: true, // Readonly related options readOnly, diff --git a/datahub/webapp/components/QueryRunButton/QueryRunButton.tsx b/datahub/webapp/components/QueryRunButton/QueryRunButton.tsx index 8e2212957..5765db453 100644 --- a/datahub/webapp/components/QueryRunButton/QueryRunButton.tsx +++ b/datahub/webapp/components/QueryRunButton/QueryRunButton.tsx @@ -24,12 +24,12 @@ interface IQueryRunButtonProps { onEngineIdSelect: (id: number) => any; } -export interface IRunButtonHandles { +export interface IQueryRunButtonHandles { clickRunButton(): void; } export const QueryRunButton = React.forwardRef< - IRunButtonHandles, + IQueryRunButtonHandles, IQueryRunButtonProps >( ( diff --git a/datahub/webapp/components/SearchAndReplace/CodeMirrorSearchHighlighter.tsx b/datahub/webapp/components/SearchAndReplace/CodeMirrorSearchHighlighter.tsx new file mode 100644 index 000000000..03f8414c7 --- /dev/null +++ b/datahub/webapp/components/SearchAndReplace/CodeMirrorSearchHighlighter.tsx @@ -0,0 +1,64 @@ +import React, { useContext, useMemo, useEffect } from 'react'; +import { SearchAndReplaceContext } from 'context/searchAndReplace'; +import { getCodemirrorOverlay } from 'lib/data-doc/search'; + +export const CodeMirrorSearchHighlighter: React.FC<{ + editor: CodeMirror.Editor; + cellId: number; +}> = ({ editor, cellId }) => { + const { + searchState: { + searchResults, + searchString, + currentSearchResultIndex, + searchOptions, + }, + } = useContext(SearchAndReplaceContext); + + const shouldHighlight = useMemo( + () => editor && searchResults.some((r) => r.cellId === cellId), + [searchResults, cellId, editor] + ); + + // highlighter + useEffect(() => { + const overlay = shouldHighlight + ? getCodemirrorOverlay(searchString, searchOptions) + : null; + + if (overlay) { + editor.addOverlay(overlay); + } + + return () => { + if (overlay) { + editor.removeOverlay(overlay); + } + }; + }, [shouldHighlight, editor, searchString, searchOptions]); + + // jump to item + const currentSearchItem = useMemo(() => { + const item = searchResults[currentSearchResultIndex]; + if (item?.cellId === cellId) { + return item; + } + return null; + }, [currentSearchResultIndex, cellId, searchResults]); + + useEffect(() => { + if (currentSearchItem && editor) { + // editor.focus(); + const doc = editor.getDoc(); + doc.setSelection( + doc.posFromIndex(currentSearchItem.from), + doc.posFromIndex(currentSearchItem.to), + { + scroll: true, + } + ); + } + }, [currentSearchResultIndex]); + + return null; +}; diff --git a/datahub/webapp/components/SearchAndReplace/DraftJsSearchHighlighter.tsx b/datahub/webapp/components/SearchAndReplace/DraftJsSearchHighlighter.tsx new file mode 100644 index 000000000..cdf6bf587 --- /dev/null +++ b/datahub/webapp/components/SearchAndReplace/DraftJsSearchHighlighter.tsx @@ -0,0 +1,93 @@ +import React, { useContext, useMemo, useEffect } from 'react'; +import * as DraftJs from 'draft-js'; +import scrollIntoView from 'smooth-scroll-into-view-if-needed'; + +import { RichTextEditor } from 'ui/RichTextEditor/RichTextEditor'; +import { SearchAndReplaceContext } from 'context/searchAndReplace'; +import { LinkDecorator } from 'lib/draft-js-utils'; +import { makeSearchHighlightDecorator } from 'components/SearchAndReplace/SearchHighlightDecorator'; + +export const DraftJsSearchHighlighter: React.FC<{ + editor: RichTextEditor; + cellId: number; +}> = ({ editor, cellId }) => { + const { + searchState: { + searchResults, + searchString, + searchOptions, + currentSearchResultIndex, + }, + focusSearchBar, + } = useContext(SearchAndReplaceContext); + + const shouldHighlight = useMemo( + () => editor && searchResults.some((r) => r.cellId === cellId), + [searchResults, cellId, editor] + ); + useEffect(() => { + if (editor) { + const decorators = [LinkDecorator]; + if (shouldHighlight) { + decorators.push( + makeSearchHighlightDecorator(searchString, searchOptions) + ); + } + + editor.editorState = DraftJs.EditorState.set(editor.editorState, { + decorator: new DraftJs.CompositeDecorator(decorators), + }); + } + }, [shouldHighlight, editor, searchString, searchOptions]); + + // jump to item + const currentSearchItem = useMemo(() => { + const item = searchResults[currentSearchResultIndex]; + if (item?.cellId === cellId) { + return item; + } + return null; + }, [currentSearchResultIndex, cellId, searchResults]); + + useEffect(() => { + if (currentSearchItem && editor) { + // editor.focus(); + const selectionState: DraftJs.SelectionState = new DraftJs.SelectionState( + { + anchorKey: currentSearchItem.blockKey, + anchorOffset: currentSearchItem.from, + focusKey: currentSearchItem.blockKey, + focusOffset: currentSearchItem.to, + hasFocus: false, + isBackward: false, + } + ); + editor.editorState = DraftJs.EditorState.forceSelection( + editor.editorState, + selectionState + ); + setTimeout(() => { + // Known issues: Pressing enter too fast + // would cause the enter to be applied to the draft js + // rich text editor, so setting a force blur after 50ms + // to prevent the editor to be accidentally edited + const element = window.getSelection().focusNode.parentElement; + editor.draftJSEditor?.blur(); + + setTimeout(() => { + // The DataDoc scrolls to the cell (sometimes its lazy loaded) + // however we also want to make sure we scroll to the element + // itself, so added a double scroll after 500ms to make sure + // it happens after the DataDoc scroll + scrollIntoView(element, { + scrollMode: 'if-needed', + duration: 0, + }); + focusSearchBar(); + }, 50); + }, 50); + } + }, [currentSearchResultIndex]); + + return null; +}; diff --git a/datahub/webapp/components/SearchAndReplace/SearchAndReplace.tsx b/datahub/webapp/components/SearchAndReplace/SearchAndReplace.tsx new file mode 100644 index 000000000..e07a64e0c --- /dev/null +++ b/datahub/webapp/components/SearchAndReplace/SearchAndReplace.tsx @@ -0,0 +1,271 @@ +import React, { + useState, + useEffect, + useCallback, + useReducer, + useRef, + useImperativeHandle, +} from 'react'; +import { + ISearchOptions, + ISearchResult, + ISearchAndReplaceState, +} from 'const/searchAndReplace'; +import { + ISearchAndReplaceContextType, + SearchAndReplaceContext, +} from 'context/searchAndReplace'; +import { + ISearchAndReplaceBarProps, + SearchAndReplaceBar, +} from 'components/SearchAndReplace/SearchAndReplaceBar'; +import { WithOptional } from 'lib/typescript'; +import { ISearchAndReplaceBarHandles } from './SearchAndReplaceBar'; +import { matchKeyPress } from 'lib/utils/keyboard'; +import { useWindowEvent } from 'hooks/useWindowEvent'; + +const initialSearchState: ISearchAndReplaceState = { + searchString: '', + replaceString: '', + searchResults: [], + currentSearchResultIndex: 0, + searchOptions: { + matchCase: false, + useRegex: false, + }, +}; + +export interface ISearchAndReplaceProps { + getSearchResults: ( + searchString: string, + searchOptions: ISearchOptions + ) => ISearchResult[]; + replace: ( + searchResultsToReplace: ISearchResult[], + replaceString: string + ) => void; + jumpToResult?: (searchResult: ISearchResult) => Promise; +} + +export function useSearchAndReplace({ + getSearchResults, + replace, + jumpToResult, + focusSearchBar, +}: ISearchAndReplaceProps & { + focusSearchBar: () => any; +}): { + searchAndReplaceContext: ISearchAndReplaceContextType; + searchAndReplaceProps: WithOptional; + reset: () => void; + performSearch: (isActiveSearch?: boolean) => void; +} { + const [searchState, setSearchState] = useState(initialSearchState); + + const reset = useCallback(() => setSearchState(initialSearchState), []); + const performSearch = useCallback( + // Active search happens when user is using the search input + // Passive search is when the search content is changed + (isActiveSearch: boolean = false) => { + setSearchState((oldSearchState) => { + const searchResults = getSearchResults( + oldSearchState.searchString, + oldSearchState.searchOptions + ); + if (isActiveSearch) { + jumpToResult( + searchResults[oldSearchState.currentSearchResultIndex] + ).then(focusSearchBar); + } + return { + ...oldSearchState, + searchResults, + }; + }); + }, + [getSearchResults, jumpToResult, focusSearchBar] + ); + const onSearchStringChange = useCallback((searchString: string) => { + setSearchState((oldSearchState) => ({ + ...oldSearchState, + searchString, + currentSearchResultIndex: 0, + })); + }, []); + const onSearchOptionsChange = useCallback( + (searchOptions: ISearchOptions) => { + setSearchState((oldSearchState) => ({ + ...oldSearchState, + searchOptions, + currentSearchResultIndex: 0, + })); + }, + [] + ); + const onReplaceStringChange = useCallback((replaceString: string) => { + setSearchState((oldSearchState) => ({ + ...oldSearchState, + replaceString, + })); + }, []); + + const moveResultIndex = useCallback((delta: number) => { + return new Promise((resolve) => { + setSearchState((oldSearchState) => { + const resultLen = oldSearchState.searchResults.length; + if (!resultLen) { + resolve(); + return oldSearchState; + } + + const currIndex = oldSearchState.currentSearchResultIndex; + let newIndex = currIndex + delta; + // Clip new index to be between [0, searchState.searchResults.length) + newIndex = + (newIndex < 0 ? newIndex + resultLen : newIndex) % + resultLen; + + if (jumpToResult) { + jumpToResult( + oldSearchState.searchResults[newIndex] + ).then(() => resolve()); + } else { + resolve(); + } + + return { + ...oldSearchState, + currentSearchResultIndex: newIndex, + }; + }); + }); + }, []); + + const onReplace = useCallback( + (all: boolean = false) => { + if (all) { + replace(searchState.searchResults, searchState.replaceString); + } else { + replace( + [ + searchState.searchResults[ + searchState.currentSearchResultIndex + ], + ], + searchState.replaceString + ); + // Special case last item is replaced + if ( + searchState.currentSearchResultIndex === + searchState.searchResults.length - 1 + ) { + // we loop over to the first item + moveResultIndex(1); + } + } + }, + [ + replace, + searchState.searchResults, + searchState.replaceString, + searchState.currentSearchResultIndex, + moveResultIndex, + ] + ); + + useEffect(() => { + performSearch(true); + }, [searchState.searchString, searchState.searchOptions]); + + return { + searchAndReplaceContext: { + searchState, + focusSearchBar, + }, + searchAndReplaceProps: { + onSearchStringChange, + onReplaceStringChange, + onReplace, + onSearchOptionsChange, + moveResultIndex, + }, + reset, + performSearch, + }; +} + +export interface ISearchAndReplaceHandles { + performSearch: (isActiveSearch?: boolean) => void; + reset: () => void; +} + +export const SearchAndReplace: React.FC< + ISearchAndReplaceProps & { + ref: React.Ref; + } +> = React.forwardRef( + ({ getSearchResults, jumpToResult, replace, children }, ref) => { + const [showSearchAndReplace, setShowSearchAndReplace] = useState(false); + const searchBarRef = useRef(null); + const focusSearchBar = useCallback(() => { + searchBarRef.current?.focus(); + }, []); + const { + searchAndReplaceContext, + searchAndReplaceProps, + performSearch, + reset, + } = useSearchAndReplace({ + getSearchResults, + replace, + jumpToResult, + focusSearchBar, + }); + + useImperativeHandle( + ref, + () => ({ + performSearch, + reset, + }), + [performSearch, reset] + ); + + const onKeyDown = useCallback( + (evt: KeyboardEvent) => { + let handled = true; + if (matchKeyPress(evt, 'Cmd-F')) { + if (!showSearchAndReplace) { + setShowSearchAndReplace(true); + } else { + focusSearchBar(); + } + } else if (showSearchAndReplace && matchKeyPress(evt, 'esc')) { + setShowSearchAndReplace(false); + } else { + handled = false; + } + + if (handled) { + evt.stopPropagation(); + evt.preventDefault(); + } + }, + [showSearchAndReplace, focusSearchBar] + ); + useWindowEvent('keydown', onKeyDown); + + return ( + + {showSearchAndReplace ? ( + setShowSearchAndReplace(false)} + /> + ) : null} + {children} + + ); + } +); diff --git a/datahub/webapp/components/SearchAndReplace/SearchAndReplaceBar.scss b/datahub/webapp/components/SearchAndReplace/SearchAndReplaceBar.scss new file mode 100644 index 000000000..0e52f7b9a --- /dev/null +++ b/datahub/webapp/components/SearchAndReplace/SearchAndReplaceBar.scss @@ -0,0 +1,50 @@ +.SearchAndReplaceBar { + border: var(--outer-border); + border-top: none; + border-bottom-left-radius: var(--border-radius); + border-bottom-right-radius: var(--border-radius); + + background-color: var(--bg-color); + + .position-info { + user-select: none; + font-size: var(--xsmall-text-size); + } + + .datadoc-search-input { + border: var(--outer-border); + border-radius: var(--border-radius); + + background-color: transparent; + width: 250px; + display: flex; + align-items: center; + padding: 4px; + + &:focus-within { + border: 1px solid var(--color-accent); + background-color: var(--bg-color); + } + + .DebouncedInput { + flex: 1; + input { + padding: none; + border: none; + color: inherit; + font-size: var(--text-size); + } + } + + .TextToggleButton { + font-size: var(--small-text-size); + color: var(--light-text-color); + font-weight: var(--bold-font); + user-select: none; + + &.active { + color: var(--dark-text-color); + } + } + } +} diff --git a/datahub/webapp/components/SearchAndReplace/SearchAndReplaceBar.tsx b/datahub/webapp/components/SearchAndReplace/SearchAndReplaceBar.tsx new file mode 100644 index 000000000..e568e707b --- /dev/null +++ b/datahub/webapp/components/SearchAndReplace/SearchAndReplaceBar.tsx @@ -0,0 +1,230 @@ +import React, { + useState, + useContext, + useCallback, + useRef, + useImperativeHandle, + useMemo, +} from 'react'; +import { throttle } from 'lodash'; +import classNames from 'classnames'; + +import { IconButton } from 'ui/Button/IconButton'; +import { DebouncedInput } from 'ui/DebouncedInput/DebouncedInput'; +import { Button } from 'ui/Button/Button'; +import { matchKeyPress } from 'lib/utils/keyboard'; +import { ISearchOptions } from 'const/searchAndReplace'; +import { SearchAndReplaceContext } from 'context/searchAndReplace'; + +import './SearchAndReplaceBar.scss'; + +export interface ISearchAndReplaceBarProps { + onHide: () => any; + + onSearchStringChange: (s: string) => any; + onSearchOptionsChange: (options: ISearchOptions) => any; + moveResultIndex: (delta: number) => Promise; + + onReplaceStringChange: (s: string) => any; + onReplace: (all?: boolean) => any; +} + +export interface ISearchAndReplaceBarHandles { + focus(): void; +} + +export const SearchAndReplaceBar = React.forwardRef< + ISearchAndReplaceBarHandles, + ISearchAndReplaceBarProps +>( + ( + { + onHide, + onSearchStringChange, + onReplaceStringChange, + moveResultIndex, + onReplace, + onSearchOptionsChange, + }, + ref + ) => { + const [showReplace, setShowReplace] = useState(false); + const { + searchState: { + searchString, + searchResults, + replaceString, + currentSearchResultIndex, + searchOptions, + }, + } = useContext(SearchAndReplaceContext); + const searchInputRef = useRef(null); + const replaceInputRef = useRef(null); + const focusSearchInput = useCallback(() => { + // To prevent the case when typing in search and then tab to go to replace + // but then searching would then refocus to search input + if (document.activeElement !== replaceInputRef.current) { + searchInputRef.current?.focus(); + } + }, []); + + // Throttling because if you press enter to focus it + // might edit the cells underneath. + const onEnterPressThrottled = useMemo( + () => + throttle(() => { + moveResultIndex(1).then(() => { + focusSearchInput(); + }); + }, 50), + [moveResultIndex] + ); + + const onKeyDown = useCallback( + (evt: React.KeyboardEvent) => { + if (matchKeyPress(evt, 'Enter') && !evt.repeat) { + evt.stopPropagation(); + onEnterPressThrottled(); + } + }, + [moveResultIndex] + ); + + useImperativeHandle(ref, () => ({ + focus: () => { + focusSearchInput(); + }, + })); + + const searchRow = ( +
    +
    + + + onSearchOptionsChange({ + ...searchOptions, + matchCase, + }) + } + tooltip="Match Case" + /> + + onSearchOptionsChange({ + ...searchOptions, + useRegex, + }) + } + tooltip="Use Regex" + /> +
    + + + {searchResults.length + ? `${currentSearchResultIndex + 1} of ${ + searchResults.length + }` + : 'No results'} + + + moveResultIndex(-1)} + /> + moveResultIndex(1)} + /> + +
    + ); + const replaceRow = showReplace && ( +
    +
    + +
    + +
    + ); + + return ( +
    + setShowReplace(!showReplace)} + /> +
    + {searchRow} + {replaceRow} +
    +
    + ); + } +); + +const TextToggleButton: React.FC<{ + value: boolean; + onChange: (v: boolean) => any; + text: string; + tooltip: string; +}> = ({ value, onChange, text, tooltip }) => { + const className = classNames({ + TextToggleButton: true, + active: value, + mh4: true, + }); + return ( + onChange(!value)} + aria-label={tooltip} + data-balloon-pos="down" + > + {text} + + ); +}; diff --git a/datahub/webapp/components/SearchAndReplace/SearchHighlightDecorator.tsx b/datahub/webapp/components/SearchAndReplace/SearchHighlightDecorator.tsx new file mode 100644 index 000000000..f09d4a19b --- /dev/null +++ b/datahub/webapp/components/SearchAndReplace/SearchHighlightDecorator.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { findSearchEntities } from 'lib/data-doc/search'; +import { ISearchOptions } from 'const/searchAndReplace'; + +export function makeSearchHighlightDecorator( + searchString: string, + searchOptions: ISearchOptions +) { + return { + strategy: findSearchEntities(searchString, searchOptions), + component: SearchHighlight, + }; +} + +const SearchHighlight: React.FC = ({ children }) => { + return {children}; +}; diff --git a/datahub/webapp/const/datadoc.ts b/datahub/webapp/const/datadoc.ts index 331682f97..d02ff830f 100644 --- a/datahub/webapp/const/datadoc.ts +++ b/datahub/webapp/const/datadoc.ts @@ -48,6 +48,7 @@ export interface IDataChartCell extends IDataCellBase { } export type IDataCell = IDataQueryCell | IDataTextCell | IDataChartCell; +export type DataCellUpdateFields = Partial>; export interface IDataDoc { dataDocCells: IDataCell[]; diff --git a/datahub/webapp/const/searchAndReplace.ts b/datahub/webapp/const/searchAndReplace.ts new file mode 100644 index 000000000..c178a5f06 --- /dev/null +++ b/datahub/webapp/const/searchAndReplace.ts @@ -0,0 +1,19 @@ +export interface ISearchResult { + cellId?: number; + blockKey?: string; + from: number; + to: number; +} + +export interface ISearchOptions { + matchCase: boolean; + useRegex: boolean; +} + +export interface ISearchAndReplaceState { + searchString: string; + searchOptions: ISearchOptions; + replaceString: string; + searchResults: ISearchResult[]; + currentSearchResultIndex: number; +} diff --git a/datahub/webapp/context/DataDoc.ts b/datahub/webapp/context/DataDoc.ts index 721e4e885..7333190e1 100644 --- a/datahub/webapp/context/DataDoc.ts +++ b/datahub/webapp/context/DataDoc.ts @@ -1,5 +1,9 @@ import React from 'react'; -import { IDataCellMeta } from 'const/datadoc'; +import { + IDataCellMeta, + IDataDocSearchState, + DataCellUpdateFields, +} from 'const/datadoc'; export interface IDataDocContextType { cellIdToExecutionId: Record; @@ -10,7 +14,8 @@ export interface IDataDocContextType { cellKey: string, context: string, meta: IDataCellMeta - ) => any; + ) => Promise; + updateCell: (cellId: number, fields: DataCellUpdateFields) => Promise; defaultCollapse: boolean; focusedCellIndex?: number; diff --git a/datahub/webapp/context/searchAndReplace.ts b/datahub/webapp/context/searchAndReplace.ts new file mode 100644 index 000000000..67d693b2e --- /dev/null +++ b/datahub/webapp/context/searchAndReplace.ts @@ -0,0 +1,11 @@ +import React from 'react'; +import { ISearchAndReplaceState } from 'const/searchAndReplace'; + +export interface ISearchAndReplaceContextType { + searchState: ISearchAndReplaceState; + focusSearchBar: () => any; +} + +export const SearchAndReplaceContext = React.createContext< + ISearchAndReplaceContextType +>(null); diff --git a/datahub/webapp/lib/codemirror/codemirror-hover.ts b/datahub/webapp/lib/codemirror/codemirror-hover.ts index d26d396ab..61ade43fa 100644 --- a/datahub/webapp/lib/codemirror/codemirror-hover.ts +++ b/datahub/webapp/lib/codemirror/codemirror-hover.ts @@ -44,7 +44,6 @@ class TextHoverState { const pos = cm.coordsChar(mouseCoord); const token = cm.getTokenAt(pos); - // console.log(mouseCoord, pos, token); if (!token) { return; } diff --git a/datahub/webapp/lib/codemirror/editor_styles.scss b/datahub/webapp/lib/codemirror/editor_styles.scss new file mode 100644 index 000000000..3e93b7911 --- /dev/null +++ b/datahub/webapp/lib/codemirror/editor_styles.scss @@ -0,0 +1,24 @@ +.CodeMirror { + font-family: var(--family-monospace); + font-size: var(--small-text-size); + line-height: 17px; + + &.cm-s-default { + &.CodeHighlight, + .CodeMirror-scroll { + background-color: var(--query-editor-default-bg-color); + } + .CodeMirror-gutters { + background-color: var(--query-editor-default-gutter-color); + border-right: 1px transparent; + } + } + + .cm-matchhighlight { + border-bottom: 1px solid var(--color-accent-bg); + } + + .cm-searching { + background-color: var(--text-highlight); + } +} diff --git a/datahub/webapp/lib/codemirror/index.ts b/datahub/webapp/lib/codemirror/index.ts index 23622bed3..bd1ef8bbd 100644 --- a/datahub/webapp/lib/codemirror/index.ts +++ b/datahub/webapp/lib/codemirror/index.ts @@ -31,6 +31,12 @@ import 'codemirror/addon/edit/closebrackets'; import 'lib/codemirror/codemirror-hover'; import 'codemirror/addon/runmode/runmode'; +// Search highlighting +import 'codemirror/addon/search/match-highlighter.js'; + +// Local styling +import './editor_styles.scss'; + declare module 'codemirror' { // This is copied from runmode.d.ts. Not sure how to import it :( function runMode( diff --git a/datahub/webapp/lib/data-doc/data-doc-utils.ts b/datahub/webapp/lib/data-doc/data-doc-utils.ts index 1812c1ed8..f37946fe0 100644 --- a/datahub/webapp/lib/data-doc/data-doc-utils.ts +++ b/datahub/webapp/lib/data-doc/data-doc-utils.ts @@ -1,19 +1,25 @@ -import { IDataCell } from 'const/datadoc'; -import { scrollToElement } from 'lib/utils'; +import scrollIntoView from 'smooth-scroll-into-view-if-needed'; export async function scrollToCell( - dataDocCell: IDataCell, + cellId: number, duration: number = 200, repeat: number = 1 ) { - const anchorName = getAnchorNameForCell(dataDocCell.id); + if (cellId == null) { + return; + } + const anchorName = getAnchorNameForCell(cellId); + const element = document.getElementById(anchorName); for (let i = 0; i < repeat; i++) { - await scrollToElement(document.getElementById(anchorName), duration); - } + await scrollIntoView(element, { + behavior: 'smooth', + scrollMode: 'if-needed', + block: 'start', - location.hash = ''; - location.hash = anchorName; + duration, + }); + } } export function getAnchorNameForCell(cellKey: string | number) { diff --git a/datahub/webapp/lib/data-doc/search.ts b/datahub/webapp/lib/data-doc/search.ts new file mode 100644 index 000000000..aa33d595e --- /dev/null +++ b/datahub/webapp/lib/data-doc/search.ts @@ -0,0 +1,236 @@ +import * as DraftJs from 'draft-js'; +import { IDataDoc } from 'const/datadoc'; +import { ISearchResult, ISearchOptions } from 'const/searchAndReplace'; + +export function searchText( + text: string, + searchString: string, + options: ISearchOptions +) { + const searchRegex = getSearchRegex(searchString, options); + const results: ISearchResult[] = []; + + if (!searchRegex) { + return results; + } + + let match: RegExpExecArray; + // tslint:disable-next-line + while ((match = searchRegex.exec(text)) !== null) { + results.push({ + from: match.index, + to: match.index + match[0].length, + }); + } + + return results; +} + +export function searchDataDoc( + dataDoc: IDataDoc, + searchString: string, + options: ISearchOptions +) { + const searchRegex = getSearchRegex(searchString, options); + const results: ISearchResult[] = []; + + if (!searchRegex) { + return results; + } + for (const cell of dataDoc.dataDocCells) { + if (cell.cell_type === 'query') { + const content = cell.context; + let match: RegExpExecArray; + // tslint:disable-next-line + while ((match = searchRegex.exec(content)) !== null) { + results.push({ + cellId: cell.id, + from: match.index, + to: match.index + match[0].length, + }); + } + } else if (cell.cell_type === 'text') { + const content = cell.context; + content.getBlockMap().map((contentBlock) => { + let match: RegExpExecArray; + const blockText = contentBlock.getText(); + // tslint:disable-next-line + while ((match = searchRegex.exec(blockText)) !== null) { + results.push({ + cellId: cell.id, + blockKey: contentBlock.getKey(), + from: match.index, + to: match.index + match[0].length, + }); + } + }); + } + } + + return results; +} + +function getSearchRegex(searchString: string, searchOptions: ISearchOptions) { + if (!searchString) { + return null; + } + + let searchRegex = searchString; + if (!searchOptions.useRegex) { + // escape all regex parameters + searchRegex = searchRegex.replace( + /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, + '\\$&' + ); + } + + let flags = 'g'; + if (!searchOptions.matchCase) { + flags += 'i'; + } + + return new RegExp(searchRegex, flags); +} + +export function getCodemirrorOverlay( + searchString: string, + searchOptions: ISearchOptions +) { + const searchRegex = getSearchRegex(searchString, searchOptions); + return { + token: (stream: CodeMirror.StringStream) => { + if (!searchRegex) { + stream.skipToEnd(); + return; + } + + searchRegex.lastIndex = stream.pos; + const match = searchRegex.exec(stream.string); + if (match && match.index === stream.pos) { + stream.pos += match[0].length || 1; + return 'searching'; + } else if (match) { + stream.pos = match.index; + } else { + stream.skipToEnd(); + } + }, + }; +} + +export const findSearchEntities = ( + searchString: string, + searchOptions: ISearchOptions +) => { + const searchRegex = getSearchRegex(searchString, searchOptions); + return ( + contentBlock: DraftJs.ContentBlock, + callback: (start: number, end: number) => void + ) => { + if (!searchRegex) { + return; + } + + let match: RegExpExecArray; + const blockText = contentBlock.getText(); + // tslint:disable-next-line + while ((match = searchRegex.exec(blockText)) !== null) { + callback(match.index, match.index + match[0].length); + } + }; +}; + +// Move this to a utils function? +export function replaceStringIndices( + s: string, + indices: Array<[number, number]>, + replaceString: string +) { + const replaceLen = replaceString.length; + let offset = 0; + let ret = s; + + for (const pair of indices) { + const start = pair[0] + offset; + const end = pair[1] + offset; + const len = pair[1] - pair[0]; + + ret = ret.substr(0, start) + replaceString + ret.substr(end); + offset += replaceLen - len; + } + + return ret; +} + +export function replaceDraftJsContent( + contentState: DraftJs.ContentState, + items: ISearchResult[], + replaceString: string +) { + const selectionsToReplace = items.map( + (item) => + new DraftJs.SelectionState({ + anchorKey: item.blockKey, + anchorOffset: item.from, + focusKey: item.blockKey, + focusOffset: item.to, + hasFocus: false, + isBackward: false, + }) + ); + + let newContentState = contentState; + for (const selection of selectionsToReplace) { + newContentState = DraftJs.Modifier.replaceText( + newContentState, + selection, + replaceString + ); + } + + return newContentState; +} + +export async function replaceDataDoc( + dataDoc: IDataDoc, + items: ISearchResult[], + replaceString: string, + onChange: ( + cellId: number, + context: string | DraftJs.ContentState + ) => Promise +) { + if (!items?.length) { + return; + } + + const allChanges: Array> = []; + for (const cell of dataDoc.dataDocCells) { + const itemsForCell = items.filter((item) => item.cellId === cell.id); + if (!itemsForCell.length) { + continue; + } + + if (cell.cell_type === 'query') { + const newString = replaceStringIndices( + cell.context, + itemsForCell.map((item) => [item.from, item.to]), + replaceString + ); + allChanges.push(onChange(cell.id, newString)); + } else if (cell.cell_type === 'text') { + allChanges.push( + onChange( + cell.id, + replaceDraftJsContent( + cell.context, + itemsForCell, + replaceString + ) + ) + ); + } + } + + await Promise.all(allChanges); +} diff --git a/datahub/webapp/lib/utils/index.ts b/datahub/webapp/lib/utils/index.ts index 796e31d8e..c63e5aae1 100644 --- a/datahub/webapp/lib/utils/index.ts +++ b/datahub/webapp/lib/utils/index.ts @@ -200,13 +200,6 @@ export function formatPlural(count: number, unit: string) { } // from https://stackoverflow.com/a/39494245 -export function scrollToElement(element: HTMLElement, duration: number) { - const scrollContainer = getScrollParent(element); - if (scrollContainer) { - return smoothScroll(scrollContainer, element.offsetTop, duration); - } -} - export function smoothScroll( scrollContainer: HTMLElement, finalScrollTop: number, diff --git a/datahub/webapp/redux/dataDoc/selector.ts b/datahub/webapp/redux/dataDoc/selector.ts index 3fae2349a..95ffd3677 100644 --- a/datahub/webapp/redux/dataDoc/selector.ts +++ b/datahub/webapp/redux/dataDoc/selector.ts @@ -7,7 +7,7 @@ import { permissionToReadWrite, } from 'lib/data-doc/datadoc-permission'; import { IStoreState } from 'redux/store/types'; -import { IDataCell, IDataDoc } from 'const/datadoc'; +import { IDataCell } from 'const/datadoc'; import { myUserInfoSelector } from 'redux/user/selector'; diff --git a/datahub/webapp/stylesheets/_html.scss b/datahub/webapp/stylesheets/_html.scss index f06dced47..a2f7c06a3 100644 --- a/datahub/webapp/stylesheets/_html.scss +++ b/datahub/webapp/stylesheets/_html.scss @@ -136,20 +136,3 @@ blockquote { ::-webkit-scrollbar-corner { background: rgba(0, 0, 0, 0); } - -.CodeMirror { - font-family: var(--family-monospace); - font-size: var(--small-text-size); - line-height: 17px; - - &.cm-s-default { - &.CodeHighlight, - .CodeMirror-scroll { - background-color: var(--query-editor-default-bg-color); - } - .CodeMirror-gutters { - background-color: var(--query-editor-default-gutter-color); - border-right: 1px transparent; - } - } -} diff --git a/datahub/webapp/stylesheets/_variables.scss b/datahub/webapp/stylesheets/_variables.scss index 8202f9ede..9c0082d75 100644 --- a/datahub/webapp/stylesheets/_variables.scss +++ b/datahub/webapp/stylesheets/_variables.scss @@ -120,6 +120,7 @@ body { --table-hover-bg-color: var(--color-primary-3); --red-highlight: rgba(189, 8, 28, 0.5); + --text-highlight: rgba(255, 255, 0, 0.5); --mix-blend-mode: multiply; @@ -209,6 +210,8 @@ body.dark-theme { --scroll-bar-color: var(--color-primary-3); --mix-blend-mode: lighten; + --text-highlight: rgba(52, 101, 127, 0.5); + font-weight: 500; } diff --git a/datahub/webapp/ui/Container/Container.tsx b/datahub/webapp/ui/Container/Container.tsx index 8f71d03e4..932a8aa05 100644 --- a/datahub/webapp/ui/Container/Container.tsx +++ b/datahub/webapp/ui/Container/Container.tsx @@ -4,7 +4,7 @@ import { FullHeight, IFullHeightProps } from 'ui/FullHeight/FullHeight'; import './Container.scss'; export const Container: React.FC< - React.HTMLAttributes & IFullHeightProps + React.HTMLProps & IFullHeightProps > = ({ children, flex, ...props }) => { return (
    diff --git a/datahub/webapp/ui/DebouncedInput/DebouncedInput.tsx b/datahub/webapp/ui/DebouncedInput/DebouncedInput.tsx index b91272490..fcba7b853 100644 --- a/datahub/webapp/ui/DebouncedInput/DebouncedInput.tsx +++ b/datahub/webapp/ui/DebouncedInput/DebouncedInput.tsx @@ -16,7 +16,7 @@ export interface IDebouncedInputProps extends IDebouncedInputStylingProps { debounceTime?: number; debounceMethod?: 'debounce' | 'throttle'; autoAdjustWidth?: boolean; - inputProps?: React.InputHTMLAttributes; + inputProps?: React.HTMLProps; onChange: (value: string) => any; } diff --git a/datahub/webapp/ui/DropdownMenu/DropdownMenu.tsx b/datahub/webapp/ui/DropdownMenu/DropdownMenu.tsx index 0062e7d37..a6a7bb903 100644 --- a/datahub/webapp/ui/DropdownMenu/DropdownMenu.tsx +++ b/datahub/webapp/ui/DropdownMenu/DropdownMenu.tsx @@ -101,7 +101,7 @@ export const DropdownMenu: React.FunctionComponent = ({ : 'far fa-circle' : 'fa fa-' + action.icon; - const actionProps: React.AnchorHTMLAttributes = {}; + const actionProps: React.HTMLProps = {}; if (action.onClick) { actionProps.onClick = action.onClick; } diff --git a/datahub/webapp/ui/FormikField/NumberField.tsx b/datahub/webapp/ui/FormikField/NumberField.tsx index e3370c212..fa98a60ae 100644 --- a/datahub/webapp/ui/FormikField/NumberField.tsx +++ b/datahub/webapp/ui/FormikField/NumberField.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { useField } from 'formik'; -export interface INumberFieldProps - extends React.InputHTMLAttributes { +export interface INumberFieldProps extends React.HTMLProps { name: string; } diff --git a/datahub/webapp/ui/RichTextEditor/RichTextEditor.scss b/datahub/webapp/ui/RichTextEditor/RichTextEditor.scss index 83feee437..22c9ec68a 100644 --- a/datahub/webapp/ui/RichTextEditor/RichTextEditor.scss +++ b/datahub/webapp/ui/RichTextEditor/RichTextEditor.scss @@ -15,5 +15,9 @@ h6 { color: var(--text-color); } + + .search-highlight { + background-color: var(--text-highlight); + } } } diff --git a/datahub/webapp/ui/RichTextEditor/RichTextEditor.tsx b/datahub/webapp/ui/RichTextEditor/RichTextEditor.tsx index fcd196a73..95b20c5b4 100644 --- a/datahub/webapp/ui/RichTextEditor/RichTextEditor.tsx +++ b/datahub/webapp/ui/RichTextEditor/RichTextEditor.tsx @@ -31,6 +31,8 @@ export interface IRichTextEditorProps { ) => any; onFocus?: () => any; onBlur?: () => any; + + decorator?: DraftJs.CompositeDecorator; } export interface IRichTextEditorState { @@ -49,7 +51,7 @@ export class RichTextEditor extends React.Component< public readonly state = { editorState: DraftJs.EditorState.createWithContent( this.props.value, - compositeDecorator + this.props.decorator ?? compositeDecorator ), toolBarStyle: { top: 0, @@ -68,6 +70,20 @@ export class RichTextEditor extends React.Component< this.forceUpdate(); } + public get editorState() { + return this.state.editorState; + } + + public set editorState(editorState: DraftJs.EditorState) { + this.setState({ + editorState, + }); + } + + public get draftJSEditor() { + return this.editorRef.current; + } + public getContent() { return this.state.editorState.getCurrentContent(); } diff --git a/datahub/webapp/ui/Title/Title.tsx b/datahub/webapp/ui/Title/Title.tsx index d97116806..5e597df7b 100644 --- a/datahub/webapp/ui/Title/Title.tsx +++ b/datahub/webapp/ui/Title/Title.tsx @@ -25,8 +25,7 @@ const StyledTitle = styled.p` } `; -export interface ITitleProps - extends React.ParamHTMLAttributes { +export interface ITitleProps extends React.HTMLProps { tooltip?: string; // styling diff --git a/package.json b/package.json index 24e163e27..b5af7a70d 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "redux-thunk": "2.3.0", "regenerator-runtime": "^0.13.2", "reselect": "4.0.0", + "smooth-scroll-into-view-if-needed": "^1.1.27", "socket.io": "2.2.0", "socket.io-client": "2.2.0", "sql-formatter": "2.3.3", @@ -101,6 +102,7 @@ "@storybook/addon-links": "^5.1.9", "@storybook/addons": "^5.1.9", "@storybook/react": "^5.1.9", + "@types/chart.js": "2.9.11", "@types/classnames": "^2.2.6", "@types/codemirror": "^0.0.76", "@types/d3": "^5.0.1", @@ -115,7 +117,6 @@ "@types/qs": "^6.5.1", "@types/rc-time-picker": "^3.4.1", "@types/react": "16.9.13", - "@types/chart.js": "2.9.11", "@types/react-dom": "16.9.4", "@types/react-lazyload": "2.6.0", "@types/react-redux": "7.1.5", diff --git a/yarn.lock b/yarn.lock index 2ecf010a1..8fa225c31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2715,6 +2715,11 @@ dependencies: "@types/tern" "*" +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + "@types/d3-array@*": version "1.2.4" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-1.2.4.tgz#7088445c8717ba1fba416a1df7bbd11cc72a3763" @@ -3206,6 +3211,11 @@ dependencies: redux "^3.6.0" +"@types/rewire@^2.5.28": + version "2.5.28" + resolved "https://registry.yarnpkg.com/@types/rewire/-/rewire-2.5.28.tgz#ff34de38c4269fe74e2597195d4918c25d42ebad" + integrity sha512-uD0j/AQOa5le7afuK+u+woi8jNKF1vf3DN0H7LCJhft/lNNibUr7VcAesdgtWfEKveZol3ZG1CJqwx2Bhrnl8w== + "@types/socket.io-client@1.4.32": version "1.4.32" resolved "https://registry.yarnpkg.com/@types/socket.io-client/-/socket.io-client-1.4.32.tgz#988a65a0386c274b1c22a55377fab6a30789ac14" @@ -3500,6 +3510,11 @@ acorn-globals@^4.1.0: acorn "^6.0.1" acorn-walk "^6.0.1" +acorn-jsx@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe" + integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ== + acorn-walk@^6.0.1: version "6.1.1" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" @@ -3520,6 +3535,11 @@ acorn@^6.2.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" integrity sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA== +acorn@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" + integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== + add-dom-event-listener@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz#6a92db3a0dd0abc254e095c0f1dc14acbbaae310" @@ -3611,6 +3631,16 @@ ajv@^6.1.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.10.0, ajv@^6.10.2: + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -3633,6 +3663,13 @@ ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== + dependencies: + type-fest "^0.11.0" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -3653,6 +3690,11 @@ ansi-regex@^4.0.0, ansi-regex@^4.1.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" @@ -3665,6 +3707,14 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -4844,7 +4894,7 @@ ccount@^1.0.3: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.0.4.tgz#9cf2de494ca84060a2a8d2854edd6dfb0445f386" integrity sha512-fpZ81yYfzentuieinmGnphk0pLkOTMm6MZdVqwd77ROvhko6iujLNGrHH5E7utq3ygWklwfmwuG+A7P+NpqT6w== -chalk@2.4.2, chalk@^2.0.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -4873,6 +4923,14 @@ chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + change-emitter@^0.1.2: version "0.1.6" resolved "https://registry.yarnpkg.com/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515" @@ -5038,6 +5096,13 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-table3@0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" @@ -5154,12 +5219,19 @@ color-convert@^1.9.0: dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@^1.0.0: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -5265,6 +5337,11 @@ compression@^1.7.4: safe-buffer "5.1.2" vary "~1.1.2" +compute-scroll-into-view@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.13.tgz#be1b1663b0e3f56cd5f7713082549f562a3477e2" + integrity sha512-o+w9w7A98aAFi/GjK8cxSV+CdASuPa2rR5UWs3+yHkJzWqaKoBEufFNWYaXInCSmUfDCVhesG+v9MTWqOjsxFg== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -6330,7 +6407,7 @@ debug@^3.2.5, debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -6782,6 +6859,11 @@ emoji-regex@^7.0.1: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -7062,6 +7144,78 @@ eslint-scope@^4.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" + integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + +eslint@^6.8.0: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" + integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.3" + eslint-visitor-keys "^1.1.0" + espree "^6.1.2" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^7.0.0" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.3" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== + dependencies: + acorn "^7.1.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.1.0" + esprima@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" @@ -7072,6 +7226,13 @@ esprima@^4.0.0, esprima@~4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +esquery@^1.0.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.1.0: version "4.2.1" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" @@ -7084,6 +7245,11 @@ estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= +estraverse@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642" + integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw== + esutils@^2.0.0, esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -7292,6 +7458,11 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + fast-glob@^2.0.2: version "2.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" @@ -7309,7 +7480,7 @@ fast-json-stable-stringify@2.0.0, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.4, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= @@ -7399,6 +7570,20 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + file-loader@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" @@ -7497,6 +7682,20 @@ find-up@^4.0.0: locate-path "^5.0.0" path-exists "^4.0.0" +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" @@ -7727,6 +7926,11 @@ function.prototype.name@^1.1.1: functions-have-names "^1.1.1" is-callable "^1.1.4" +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + functions-have-names@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" @@ -7805,6 +8009,13 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-parent@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob-to-regexp@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" @@ -7868,6 +8079,13 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.8.0.tgz#c1ef45ee9bed6badf0663c5cb90e8d1adec1321d" integrity sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA== +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + globalthis@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.0.tgz#c5fb98213a9b4595f59cf3e7074f141b4169daae" @@ -8005,6 +8223,11 @@ has-flag@^3.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + has-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" @@ -8378,6 +8601,11 @@ ignore@^3.3.5: resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" @@ -8423,6 +8651,14 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" +import-fresh@^3.0.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + import-from@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" @@ -8531,6 +8767,25 @@ inquirer@^6.2.0: strip-ansi "^5.1.0" through "^2.3.6" +inquirer@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.1.0.tgz#1298a01859883e17c7264b82870ae1034f92dd29" + integrity sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg== + dependencies: + ansi-escapes "^4.2.1" + chalk "^3.0.0" + cli-cursor "^3.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.5.3" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -8750,6 +9005,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-function@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" @@ -8781,6 +9041,13 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-hexadecimal@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.3.tgz#e8a426a69b6d31470d3a33a47bb825cda02506ee" @@ -9528,6 +9795,11 @@ json-schema@0.2.3: resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" @@ -9679,7 +9951,7 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== -levn@~0.3.0: +levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= @@ -10214,6 +10486,11 @@ mimic-fn@^1.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -10390,6 +10667,11 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + nan@^2.12.1, nan@^2.13.2: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -10913,6 +11195,13 @@ onetime@^2.0.0: dependencies: mimic-fn "^1.0.0" +onetime@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== + dependencies: + mimic-fn "^2.1.0" + open@^6.1.0: version "6.3.0" resolved "https://registry.yarnpkg.com/open/-/open-6.3.0.tgz#60d0b845ee38fae0631f5d739a21bd40e3d2a527" @@ -10954,6 +11243,18 @@ optionator@^0.8.1: type-check "~0.3.2" wordwrap "~1.0.0" +optionator@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + original@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" @@ -11117,6 +11418,13 @@ param-case@2.1.x: dependencies: no-case "^2.2.0" +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + parse-asn1@^5.0.0: version "5.1.1" resolved "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz#f6bf293818332bd0dab54efb16087724745e6ca8" @@ -11864,6 +12172,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -12812,6 +13125,11 @@ regexp.prototype.flags@^1.2.0: dependencies: define-properties "^1.1.2" +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + regexpu-core@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b" @@ -13010,6 +13328,11 @@ resolve-from@^3.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" integrity sha1-six699nWiBvItuZTM17rywoYh0g= +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + resolve-from@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" @@ -13059,6 +13382,14 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -13069,6 +13400,13 @@ retry@^0.12.0: resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= +rewire@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/rewire/-/rewire-5.0.0.tgz#c4e6558206863758f6234d8f11321793ada2dbff" + integrity sha512-1zfitNyp9RH5UDyGGLe9/1N0bMlPQ0WrX0Tmg11kMHBpqwPJI4gfPpP7YngFyLbFmhXh19SToAG0sKKEFcOIJA== + dependencies: + eslint "^6.8.0" + rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" @@ -13076,7 +13414,7 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1: dependencies: glob "^7.0.5" -rimraf@^2.6.3: +rimraf@2.6.3, rimraf@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -13111,6 +13449,11 @@ run-async@^2.2.0: dependencies: is-promise "^2.1.0" +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + run-queue@^1.0.0, run-queue@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" @@ -13130,6 +13473,13 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" +rxjs@^6.5.3: + version "6.5.5" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec" + integrity sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -13234,6 +13584,13 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" +scroll-into-view-if-needed@^2.2.24: + version "2.2.24" + resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.24.tgz#12bca532990769bd509115a49edcfa755e92a0ea" + integrity sha512-vsC6SzyIZUyJG8o4nbUDCiIwsPdH6W/FVmjT2avR2hp/yzS53JjGmg/bKD20TkoNajbu5dAQN4xR7yes4qhwtQ== + dependencies: + compute-scroll-into-view "^1.0.13" + scroll-smooth@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/scroll-smooth/-/scroll-smooth-1.0.1.tgz#77be38594dc0ae4af58851383927b60ad9f3f842" @@ -13294,7 +13651,7 @@ semver@^6.0.0, semver@^6.1.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.1.2.tgz#079960381376a3db62eb2edc8a3bfb10c7cfe318" integrity sha512-z4PqiCpomGtWj8633oeAdXm1Kn1W++3T8epkZYnwiVgIYIJ0QHszhInYSJTYxebByQH7KVCEAn8R9duzZW2PhQ== -semver@^6.2.0: +semver@^6.1.2, semver@^6.2.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -13523,6 +13880,22 @@ slash@^2.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +smooth-scroll-into-view-if-needed@^1.1.27: + version "1.1.27" + resolved "https://registry.yarnpkg.com/smooth-scroll-into-view-if-needed/-/smooth-scroll-into-view-if-needed-1.1.27.tgz#88405e84448a9d3dd4e2c94f970e61d4d7187374" + integrity sha512-1BUbpRHzwro4MNhNpYCPA+d7G77G8k89UXPSx2A+UeJoAt3WiqrUHwT2oskXnir3p0wc4VTiRj41PQN9TEEIUw== + dependencies: + scroll-into-view-if-needed "^2.2.24" + snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -13892,6 +14265,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + string.prototype.matchall@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-3.0.1.tgz#5a9e0b64bcbeb336aa4814820237c2006985646d" @@ -13986,6 +14368,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -14010,6 +14399,11 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" +strip-json-comments@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" + integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" @@ -14079,6 +14473,13 @@ supports-color@^6.1.0: dependencies: has-flag "^3.0.0" +supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" + svgo@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.2.2.tgz#0253d34eccf2aed4ad4f283e11ee75198f9d7316" @@ -14121,6 +14522,16 @@ synchronous-promise@^2.0.6: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.7.tgz#3574b3d2fae86b145356a4b89103e1577f646fe3" integrity sha512-16GbgwTmFMYFyQMLvtQjvNWh30dsFe1cAW5Fg1wm5+dg84L9Pe36mftsIRU95/W2YsISxsz/xq4VB23sqpgb/A== +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + tapable@^1.0.0, tapable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.0.tgz#0d076a172e3d9ba088fd2272b2668fb8d194b78c" @@ -14203,7 +14614,7 @@ test-exclude@^5.2.3: read-pkg-up "^4.0.0" require-main-filename "^2.0.0" -text-table@0.2.0: +text-table@0.2.0, text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= @@ -14486,11 +14897,21 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + type-fest@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" @@ -14740,6 +15161,11 @@ v8-compile-cache@^2.0.2: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw== +v8-compile-cache@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" + integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -15099,6 +15525,11 @@ widest-line@^2.0.0: dependencies: string-width "^2.1.1" +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + wordwrap@~0.0.2: version "0.0.3" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" @@ -15162,6 +15593,13 @@ write-file-atomic@2.4.1: imurmurhash "^0.1.4" signal-exit "^3.0.2" +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + ws@^5.2.0: version "5.2.2" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f"