diff --git a/package.json b/package.json index fb81ffa90..2d51f349b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "querybook", - "version": "3.33.1", + "version": "3.33.2", "description": "A Big Data Webapp", "private": true, "scripts": { @@ -84,6 +84,7 @@ "react-hot-toast": "^2.4.0", "react-json-view": "^1.21.3", "react-lazyload": "^3.2.0", + "react-mentions": "^4.4.10", "react-redux": "7.2.4", "react-router-dom": "5.1.2", "react-select": "4.3.1", diff --git a/querybook/config/elasticsearch.yaml b/querybook/config/elasticsearch.yaml index f9cd02e43..87cad14ca 100644 --- a/querybook/config/elasticsearch.yaml +++ b/querybook/config/elasticsearch.yaml @@ -212,7 +212,7 @@ tables: full_name_ngram: type: text analyzer: edge_ngram_lowercase - completion_name: + completion_name: # to be deprecated type: completion analyzer: keyword contexts: diff --git a/querybook/server/datasources/search.py b/querybook/server/datasources/search.py index 08251c86d..32ae1db26 100644 --- a/querybook/server/datasources/search.py +++ b/querybook/server/datasources/search.py @@ -130,20 +130,14 @@ def vector_search_tables( @register("/suggest//tables/", methods=["GET"]) -def suggest_tables(metastore_id, prefix, limit=10): +def suggest_tables(metastore_id, keyword, limit=10): api_assert(limit is None or limit <= 100, "Requesting too many tables") verify_metastore_permission(metastore_id) - query = construct_suggest_table_query(prefix, limit, metastore_id) + query = construct_suggest_table_query(keyword, limit, metastore_id) options = get_matching_suggestions(query, ES_CONFIG["tables"]["index_name"]) - texts = [ - "{}.{}".format( - option.get("_source", {}).get("schema", ""), - option.get("_source", {}).get("name", ""), - ) - for option in options - ] - return texts + tables = [option.get("_source", {}).get("full_name") for option in options] + return tables # /search/ but it is actually suggest diff --git a/querybook/server/lib/elasticsearch/search_utils.py b/querybook/server/lib/elasticsearch/search_utils.py index 16c179f4e..b57145340 100644 --- a/querybook/server/lib/elasticsearch/search_utils.py +++ b/querybook/server/lib/elasticsearch/search_utils.py @@ -79,8 +79,6 @@ def get_matching_suggestions(query: Union[str, Dict], index_name: str): if result is None: result = {} - options = next(iter(result.get("suggest", {}).get("suggest", [])), {}).get( - "options", [] - ) + options = result.get("hits", {}).get("hits", []) return options diff --git a/querybook/server/lib/elasticsearch/suggest_table.py b/querybook/server/lib/elasticsearch/suggest_table.py index 358c95823..eed2f14ff 100644 --- a/querybook/server/lib/elasticsearch/suggest_table.py +++ b/querybook/server/lib/elasticsearch/suggest_table.py @@ -1,17 +1,18 @@ def construct_suggest_table_query( - prefix: str, + keyword: str, limit: int, metastore_id: int, ): return { - "suggest": { - "suggest": { - "text": prefix, - "completion": { - "field": "completion_name", - "size": limit, - "contexts": {"metastore_id": metastore_id}, + "from": 0, + "size": limit, + "query": { + "bool": { + "must": { + "match": {"full_name_ngram": {"query": keyword, "operator": "and"}} }, + "filter": {"match": {"metastore_id": metastore_id}}, } }, + "_source": ["id", "full_name"], } diff --git a/querybook/server/logic/elasticsearch.py b/querybook/server/logic/elasticsearch.py index 157ec4232..16b45cfd1 100644 --- a/querybook/server/logic/elasticsearch.py +++ b/querybook/server/logic/elasticsearch.py @@ -601,6 +601,7 @@ def get_completion_name(): "name": table_name, "full_name": full_name, "full_name_ngram": full_name, + # To be deprecated: completion_name is not used anymore "completion_name": get_completion_name, "description": get_table_description, "created_at": lambda: DATETIME_TO_UTC(table.created_at), diff --git a/querybook/webapp/components/AIAssistant/AICommandBar.tsx b/querybook/webapp/components/AIAssistant/AICommandBar.tsx index 739305e6d..00c85043d 100644 --- a/querybook/webapp/components/AIAssistant/AICommandBar.tsx +++ b/querybook/webapp/components/AIAssistant/AICommandBar.tsx @@ -1,5 +1,11 @@ -import { set, uniq } from 'lodash'; -import React, { forwardRef, useCallback, useEffect, useState } from 'react'; +import { uniq } from 'lodash'; +import React, { + forwardRef, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { AICommandInput } from 'components/AIAssistant/AICommandInput'; import { AICommandResultView } from 'components/AIAssistant/AICommandResultView'; @@ -11,14 +17,11 @@ import { } from 'const/command'; import { IQueryEngine } from 'const/queryEngine'; import { CommandRunner, useCommand } from 'hooks/useCommand'; -import { useEvent } from 'hooks/useEvent'; import { useForwardedRef } from 'hooks/useForwardedRef'; import { trackClick } from 'lib/analytics'; import { TableToken } from 'lib/sql-helper/sql-lexer'; -import { matchKeyPress } from 'lib/utils/keyboard'; import { analyzeCode } from 'lib/web-worker'; -import { Button, TextButton } from 'ui/Button/Button'; -import { IconButton } from 'ui/Button/IconButton'; +import { Button } from 'ui/Button/Button'; import { Message } from 'ui/Message/Message'; import { IResizableTextareaHandles } from 'ui/ResizableTextArea/ResizableTextArea'; import { StyledText } from 'ui/StyledText/StyledText'; @@ -65,7 +68,8 @@ export const AICommandBar: React.FC = forwardRef( (cmd) => cmd.name === (query ? 'edit' : 'generate') ); const tablesInQuery = useTablesInQuery(query, queryEngine.language); - const [tables, setTables] = useState(tablesInQuery); + const [tables, setTables] = useState([]); + const [mentionedTables, setMentionedTables] = useState([]); const commandInputRef = useForwardedRef(ref); const [showPopupView, setShowPopupView] = useState(false); const [command, setCommand] = @@ -77,6 +81,14 @@ export const AICommandBar: React.FC = forwardRef( ); const [showConfirm, setShowConfirm] = useState(false); + const finalTablesToUse = useMemo(() => { + if (mentionedTables.length > 0) { + return mentionedTables; + } else { + return uniq([...tablesInQuery, ...tables]); + } + }, [tablesInQuery, mentionedTables, tables]); + const { runCommand, isRunning, @@ -86,8 +98,14 @@ export const AICommandBar: React.FC = forwardRef( } = useCommand(command, commandRunner); useEffect(() => { - setTables((tables) => uniq([...tablesInQuery, ...tables])); - }, [tablesInQuery]); + if (!query && mentionedTables.length === 1) { + onUpdateQuery( + `SELECT * FROM ${mentionedTables[0]} LIMIT 10`, + false + ); + onFormatQuery(); + } + }, [mentionedTables]); useEffect(() => { if (command.name === 'format') { @@ -96,7 +114,7 @@ export const AICommandBar: React.FC = forwardRef( } else if (command.name === 'generate' || command.name === 'edit') { setCommandKwargs({ query_engine_id: queryEngine.id, - tables: tables, + tables: finalTablesToUse, question: commandInputValue, original_query: command.name === 'generate' ? '' : query, }); @@ -122,17 +140,13 @@ export const AICommandBar: React.FC = forwardRef( aux: { mode: command.name, question: commandKwargs.question, - tables, + tables: finalTablesToUse, }, }); } }, [command, runCommand, setShowPopupView, commandKwargs]); const getCommandResultView = () => { - if (!commandResult) { - return null; - } - if (command.name === 'generate' || command.name === 'edit') { return ( = forwardRef( commandKwargs={commandKwargs} metastoreId={queryEngine.metastore_id} commandResult={commandResult} - tables={tables} + tables={finalTablesToUse} + hasMentionedTables={mentionedTables.length > 0} originalQuery={query} isStreaming={isRunning} onContinue={handleCommand} @@ -226,6 +241,9 @@ export const AICommandBar: React.FC = forwardRef( ); setCommandInputValue(inputValue); }} + mentionedTables={mentionedTables} + onMentionedTablesChange={setMentionedTables} + metastoreId={queryEngine.metastore_id} onSubmit={handleCommand} running={isRunning} cancelGeneration={cancelCommand} diff --git a/querybook/webapp/components/AIAssistant/AICommandInput.scss b/querybook/webapp/components/AIAssistant/AICommandInput.scss index da6bacd35..d281ad652 100644 --- a/querybook/webapp/components/AIAssistant/AICommandInput.scss +++ b/querybook/webapp/components/AIAssistant/AICommandInput.scss @@ -6,10 +6,6 @@ background-color: var(--bg-lightest); border-radius: var(--border-radius-sm); - &:hover { - background-color: var(--bg-hover); - } - .stars-icon { position: relative; color: var(--icon); @@ -41,15 +37,6 @@ color: var(--color-pink-dark); } - .question-text-area { - flex: 1; - min-width: auto; - line-height: 24px; - min-height: 40px; - overflow: hidden; - padding: 8px 8px; - } - .button { height: 28px; margin: 6px; @@ -59,6 +46,7 @@ .AICommandInput-popover { border-radius: var(--border-radius-sm); overflow: hidden; + box-shadow: 0 0px 4px var(--bg-dark); .command-item { display: flex; diff --git a/querybook/webapp/components/AIAssistant/AICommandInput.tsx b/querybook/webapp/components/AIAssistant/AICommandInput.tsx index 92dd6030c..6b32ff4e7 100644 --- a/querybook/webapp/components/AIAssistant/AICommandInput.tsx +++ b/querybook/webapp/components/AIAssistant/AICommandInput.tsx @@ -6,17 +6,16 @@ import React, { useRef, useState, } from 'react'; +import { Mention, MentionsInput } from 'react-mentions'; import { IQueryCellCommand } from 'const/command'; import { useForwardedRef } from 'hooks/useForwardedRef'; -import { matchKeyPress } from 'lib/utils/keyboard'; +import { KeyMap, matchKeyMap, matchKeyPress } from 'lib/utils/keyboard'; +import { SearchTableResource } from 'resource/search'; import { Button } from 'ui/Button/Button'; import { Icon } from 'ui/Icon/Icon'; import { Popover } from 'ui/Popover/Popover'; -import { - IResizableTextareaHandles, - ResizableTextArea, -} from 'ui/ResizableTextArea/ResizableTextArea'; +import { IResizableTextareaHandles } from 'ui/ResizableTextArea/ResizableTextArea'; import './AICommandInput.scss'; @@ -24,19 +23,65 @@ interface AICommandInputProps { commands: Array; placeholder?: string; running: boolean; + mentionedTables: string[]; + metastoreId: number; onCommandChange: (command: IQueryCellCommand, commandArg: string) => void; + onMentionedTablesChange: (tables: string[]) => void; onSubmit: () => void; cancelGeneration: () => void; ref: React.Ref; } +const mentionInputStyle = { + flex: 1, + control: { + minHeight: '40px', + }, + highlighter: { + border: 'none', + padding: '8px', + lineHeight: '24px', + }, + input: { + border: 'none', + padding: '8px', + lineHeight: '24px', + }, + suggestions: { + backgroundColor: 'var(--bg)', + borderRadius: '6px', + overflow: 'hidden', + zIndex: 19, + boxShadow: '0 0px 4px var(--bg-dark)', + item: { + fontSize: 'var(--xsmall-text-size)', + padding: '6px 12px', + textAlign: 'left', + backgroundColor: 'transparent', + '&focused': { + backgroundColor: 'var(--bg-hover)', + }, + }, + }, +}; + export const AICommandInput: React.FC = forwardRef( ( - { commands = [], running, onCommandChange, onSubmit, cancelGeneration }, + { + commands = [], + running, + mentionedTables, + metastoreId, + onCommandChange, + onMentionedTablesChange, + onSubmit, + cancelGeneration, + }, ref ) => { const textareaRef = useForwardedRef(ref); const anchorRef = useRef(null); + const [showCommands, setShowCommands] = useState(false); const [command, setCommand] = useState(); const [commandValue, setCommandValue] = useState(''); const [currentCommandItemIndex, setCurrentCommandItemIndex] = @@ -64,7 +109,13 @@ export const AICommandInput: React.FC = forwardRef( ); const handleChange = useCallback( - (value: string) => { + ( + evt, + value: string, + newPlainTextValue, + mentions: Array<{ id: string; display: string }> + ) => { + onMentionedTablesChange(mentions.map((mention) => mention.id)); if (command || !value.startsWith('/')) { setCommandValue(value); return; @@ -80,6 +131,7 @@ export const AICommandInput: React.FC = forwardRef( setNewCommand(newCommand); } else { setCommandValue(value); + setShowCommands(true); } }, [command, commands, setNewCommand, setCommandValue] @@ -87,7 +139,13 @@ export const AICommandInput: React.FC = forwardRef( const onKeyDown = useCallback( (event: React.KeyboardEvent) => { - if (filteredCommands.length > 0) { + let handled = true; + // Cmd + / will reset the command and value and open the command options + if (matchKeyMap(event, KeyMap.aiCommandBar.openCommands)) { + setNewCommand(undefined); + setCommandValue('/'); + setShowCommands(true); + } else if (filteredCommands.length > 0) { if ( matchKeyPress(event, 'Enter') || matchKeyPress(event, 'Tab') @@ -95,31 +153,30 @@ export const AICommandInput: React.FC = forwardRef( setNewCommand( filteredCommands[currentCommandItemIndex] ); - event.preventDefault(); - return; - } - - if (matchKeyPress(event, 'Up')) { + } else if (matchKeyPress(event, 'Up')) { setCurrentCommandItemIndex((prev) => Math.max(prev - 1, 0) ); - event.preventDefault(); - return; - } - if (matchKeyPress(event, 'Down')) { + } else if (matchKeyPress(event, 'Down')) { setCurrentCommandItemIndex((prev) => Math.min(prev + 1, filteredCommands.length - 1) ); - event.preventDefault(); - return; + } else { + handled = false; } } else if (matchKeyPress(event, 'Enter') && !event.shiftKey) { if (!running) { onSubmit(); } - event.preventDefault(); } else if (matchKeyPress(event, 'Delete') && !commandValue) { setCommand(undefined); + } else { + handled = false; + } + + if (handled) { + event.preventDefault(); + event.stopPropagation(); } }, [ @@ -139,6 +196,34 @@ export const AICommandInput: React.FC = forwardRef( ); + const loadTables = useCallback( + ( + keyword: string, + callback: ( + options: Array<{ id: string; display: string }> + ) => void + ) => { + if (!keyword) { + return; + } + + SearchTableResource.suggest(metastoreId, keyword).then( + ({ data }) => { + const filteredTableNames = data.filter( + (table) => !mentionedTables.includes(table) + ); + const tableNameOptions = filteredTableNames.map( + (table) => ({ + id: table, + display: table, + }) + ); + callback(tableNameOptions); + } + ); + }, + [metastoreId, mentionedTables] + ); return (
@@ -153,21 +238,35 @@ export const AICommandInput: React.FC = forwardRef(
{'/' + command.name}
)}
- - {filteredCommands.length > 0 && ( + onBlur={() => setShowCommands(false)} + inputRef={textareaRef} + > + `@${mention}`} + trigger="@" + data={loadTables} + style={{ + position: 'relative', + zIndex: 1, + color: 'var(--color-accent-dark)', + backgroundColor: 'var(--color-accent-lightest-0)', + borderRadius: '4px', + }} + /> + + {showCommands && filteredCommands.length > 0 && ( ; isStreaming: boolean; onContinue: () => void; @@ -28,7 +29,8 @@ export const AICommandResultView = ({ metastoreId, originalQuery, tables, - commandResult, + hasMentionedTables, + commandResult = {}, isStreaming, onContinue, onTablesChange, @@ -40,7 +42,7 @@ export const AICommandResultView = ({ const [foundTables, setFoundTables] = useState(false); useEffect(() => { - const { type, data } = commandResult; + const { type, data = {} } = commandResult; if (type === 'tables') { onTablesChange(data); @@ -133,7 +135,7 @@ export const AICommandResultView = ({ return (
- {tablesDOM} + {!hasMentionedTables && tablesDOM} {explanation &&
{explanation}
} {queryDiffDOM} {actionButtonsDOM} diff --git a/querybook/webapp/const/keyMap.ts b/querybook/webapp/const/keyMap.ts index 9958a7e5e..55fdfb71a 100644 --- a/querybook/webapp/const/keyMap.ts +++ b/querybook/webapp/const/keyMap.ts @@ -85,6 +85,12 @@ const DEFAULT_KEY_MAP = { name: 'Delete current cell', }, }, + aiCommandBar: { + openCommands: { + key: 'Cmd-/', + name: 'Open commands', + }, + }, queryEditor: { runQuery: { key: 'Shift-Enter', diff --git a/querybook/webapp/lib/utils/keyboard.ts b/querybook/webapp/lib/utils/keyboard.ts index 44ec7583f..2fd43f19d 100644 --- a/querybook/webapp/lib/utils/keyboard.ts +++ b/querybook/webapp/lib/utils/keyboard.ts @@ -9,15 +9,15 @@ const specialKeyChecker: Record boolean> = { ALT: (event: AllKeyboardEvent) => event.altKey, SHIFT: (event: AllKeyboardEvent) => event.shiftKey, - DELETE: (event: AllKeyboardEvent) => event.keyCode === 8, - TAB: (event: AllKeyboardEvent) => event.keyCode === 9, - ENTER: (event: AllKeyboardEvent) => event.keyCode === 13, - ESC: (event: AllKeyboardEvent) => event.keyCode === 27, + DELETE: (event: AllKeyboardEvent) => event.key === 'Backspace', + TAB: (event: AllKeyboardEvent) => event.key === 'Tab', + ENTER: (event: AllKeyboardEvent) => event.key === 'Enter', + ESC: (event: AllKeyboardEvent) => event.key === 'Escape', - LEFT: (event: AllKeyboardEvent) => event.keyCode === 37, - UP: (event: AllKeyboardEvent) => event.keyCode === 38, - RIGHT: (event: AllKeyboardEvent) => event.keyCode === 39, - DOWN: (event: AllKeyboardEvent) => event.keyCode === 40, + LEFT: (event: AllKeyboardEvent) => event.key === 'ArrowLeft', + UP: (event: AllKeyboardEvent) => event.key === 'ArrowUp', + RIGHT: (event: AllKeyboardEvent) => event.key === 'ArrowRight', + DOWN: (event: AllKeyboardEvent) => event.key === 'ArrowDown', }; const SpecialKeyToSymbol = { @@ -54,7 +54,7 @@ export function matchKeyPress( if (upperKey in specialKeyChecker) { return specialKeyChecker[upperKey](event); } else if (upperKey.length === 1) { - return event.keyCode === upperKey.charCodeAt(0); + return event.key.toUpperCase() === upperKey; } return false; }); diff --git a/querybook/webapp/resource/search.ts b/querybook/webapp/resource/search.ts index e22f8a927..18c5fcd5d 100644 --- a/querybook/webapp/resource/search.ts +++ b/querybook/webapp/resource/search.ts @@ -33,9 +33,9 @@ export const SearchTableResource = { count: number; }>('/search/tables/vector/', { ...params }), - suggest: (metastoreId: number, prefix: string) => + suggest: (metastoreId: number, keyword: string) => ds.fetch(`/suggest/${metastoreId}/tables/`, { - prefix, + keyword, }), }; diff --git a/yarn.lock b/yarn.lock index 4f5eb852a..7c4aa187b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2707,6 +2707,13 @@ pirates "^4.0.0" source-map-support "^0.5.16" +"@babel/runtime@7.4.5": + version "7.4.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12" + integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.0.0": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d" @@ -2763,6 +2770,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.3.4": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.5.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6": version "7.10.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" @@ -11842,7 +11856,7 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -invariant@^2.2.3: +invariant@^2.2.3, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== @@ -16120,6 +16134,16 @@ react-lifecycles-compat@^3.0.4: resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== +react-mentions@^4.4.10: + version "4.4.10" + resolved "https://registry.yarnpkg.com/react-mentions/-/react-mentions-4.4.10.tgz#ae6c1e310a405597e83ce786f12c5bfb93b097ce" + integrity sha512-JHiQlgF1oSZR7VYPjq32wy97z1w1oE4x10EuhKjPr4WUKhVzG1uFQhQjKqjQkbVqJrmahf+ldgBTv36NrkpKpA== + dependencies: + "@babel/runtime" "7.4.5" + invariant "^2.2.4" + prop-types "^15.5.8" + substyle "^9.1.0" + react-redux@7.2.4: version "7.2.4" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.4.tgz#1ebb474032b72d806de2e0519cd07761e222e225" @@ -16472,6 +16496,11 @@ regenerator-runtime@^0.13.7: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + regenerator-transform@^0.14.2: version "0.14.5" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.5.tgz#c98da154683671c9c4dcb16ece736517e1b7feb4" @@ -17971,6 +18000,14 @@ stylis@^4.0.3: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240" integrity sha512-m3k+dk7QeJw660eIKRRn3xPF6uuvHs/FFzjX3HQ5ove0qYsiygoAhwn5a3IYKaZPo5LrYD0rfVmtv1gNY1uYwg== +substyle@^9.1.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/substyle/-/substyle-9.4.1.tgz#6a4647f363bc14fecc51aac371d4dbeda082aa50" + integrity sha512-VOngeq/W1/UkxiGzeqVvDbGDPM8XgUyJVWjrqeh+GgKqspEPiLYndK+XRcsKUHM5Muz/++1ctJ1QCF/OqRiKWA== + dependencies: + "@babel/runtime" "^7.3.4" + invariant "^2.2.4" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7"