diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx index acda32b3b505c..f6ddaa76c24cd 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-graph/AssetGraphExplorer.tsx @@ -19,6 +19,7 @@ import {useCallback, useLayoutEffect, useMemo, useState} from 'react'; import {FeatureFlag} from 'shared/app/FeatureFlags.oss'; import {AssetGraphAssetSelectionInput} from 'shared/asset-graph/AssetGraphAssetSelectionInput.oss'; import {useAssetGraphExplorerFilters} from 'shared/asset-graph/useAssetGraphExplorerFilters.oss'; +import {AssetSelectionInput} from 'shared/asset-selection/input/AssetSelectionInput.oss'; import styled from 'styled-components'; import {AssetEdges} from './AssetEdges'; @@ -53,7 +54,6 @@ import { import {AssetLocation, useFindAssetLocation} from './useFindAssetLocation'; import {featureEnabled} from '../app/Flags'; import {AssetLiveDataRefreshButton} from '../asset-data/AssetLiveDataProvider'; -import {AssetSelectionInput} from '../asset-selection/input/AssetSelectionInput'; import {LaunchAssetExecutionButton} from '../assets/LaunchAssetExecutionButton'; import {AssetKey} from '../assets/types'; import {DEFAULT_MAX_ZOOM, SVGViewport} from '../graph/SVGViewport'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/AssetSelectionInput.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/AssetSelectionInput.oss.tsx similarity index 61% rename from js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/AssetSelectionInput.tsx rename to js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/AssetSelectionInput.oss.tsx index 19040f4c7daf9..25ad03db8db99 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/AssetSelectionInput.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/AssetSelectionInput.oss.tsx @@ -1,12 +1,12 @@ import type {Linter} from 'codemirror/addon/lint/lint'; -import {useMemo} from 'react'; import { AssetSelectionLexer, AssetSelectionParser, } from 'shared/asset-selection/AssetSelectionAntlr.oss'; -import {createUseAssetSelectionAutoComplete as defaultCreateUseAssetSelectionAutoComplete} from 'shared/asset-selection/input/useAssetSelectionAutoComplete.oss'; +import {useAssetSelectionAutoCompleteProvider as defaultUseAssetSelectionAutoCompleteProvider} from 'shared/asset-selection/input/useAssetSelectionAutoCompleteProvider.oss'; import {AssetGraphQueryItem} from '../../asset-graph/useAssetGraphData'; +import {SelectionAutoCompleteProvider} from '../../selection/SelectionAutoCompleteProvider'; import {SelectionAutoCompleteInput} from '../../selection/SelectionInput'; import {createSelectionLinter} from '../../selection/createSelectionLinter'; @@ -15,7 +15,9 @@ interface AssetSelectionInputProps { value: string; onChange: (value: string) => void; linter?: Linter; - createUseAssetSelectionAutoComplete?: typeof defaultCreateUseAssetSelectionAutoComplete; + useAssetSelectionAutoCompleteProvider?: ( + assets: AssetGraphQueryItem[], + ) => SelectionAutoCompleteProvider; } const defaultLinter = createSelectionLinter({ @@ -28,17 +30,14 @@ export const AssetSelectionInput = ({ onChange, assets, linter = defaultLinter, - createUseAssetSelectionAutoComplete = defaultCreateUseAssetSelectionAutoComplete, + useAssetSelectionAutoCompleteProvider = defaultUseAssetSelectionAutoCompleteProvider, }: AssetSelectionInputProps) => { - const useAssetSelectionAutoComplete = useMemo( - () => createUseAssetSelectionAutoComplete(assets), - [assets, createUseAssetSelectionAutoComplete], - ); + const SelectionAutoCompleteProvider = useAssetSelectionAutoCompleteProvider(assets); return ( getAttributesMap(assets), []); - return { - autoCompleteResults: useSelectionInputAutoComplete({ - nameBase: 'key', - attributesMap, - functions: FUNCTIONS, - value, - cursor, - }), - loading: false, - }; - }; -} diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/useAssetSelectionAutoCompleteProvider.oss.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/useAssetSelectionAutoCompleteProvider.oss.tsx new file mode 100644 index 0000000000000..1ed65d726433b --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/useAssetSelectionAutoCompleteProvider.oss.tsx @@ -0,0 +1,40 @@ +import {useMemo} from 'react'; + +import {attributeToIcon, getAttributesMap} from './util'; +import {AssetGraphQueryItem} from '../../asset-graph/useAssetGraphData'; +import {createSelectionAutoComplete} from '../../selection/SelectionAutoComplete'; +import { + SelectionAutoCompleteProvider, + createProvider, +} from '../../selection/SelectionAutoCompleteProvider'; + +export function useAssetSelectionAutoCompleteProvider( + assets: AssetGraphQueryItem[], +): SelectionAutoCompleteProvider { + const attributesMap = useMemo(() => getAttributesMap(assets), [assets]); + + const baseProvider = useMemo( + () => + createProvider({ + attributesMap, + primaryAttributeKey: 'key', + attributeToIcon, + }), + [attributesMap], + ); + const selectionHint = useMemo(() => createSelectionAutoComplete(baseProvider), [baseProvider]); + + return { + ...baseProvider, + useAutoComplete: ({line, cursorIndex}) => { + const autoCompleteResults = useMemo( + () => selectionHint(line, cursorIndex), + [line, cursorIndex], + ); + return { + autoCompleteResults, + loading: false, + }; + }, + }; +} diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/useAssetSelectionInput.tsx b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/useAssetSelectionInput.tsx index 2adbaaf6f3bad..a1d06f3e2ca7d 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/useAssetSelectionInput.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/useAssetSelectionInput.tsx @@ -1,9 +1,9 @@ import {FeatureFlag} from 'shared/app/FeatureFlags.oss'; import {AssetGraphAssetSelectionInput} from 'shared/asset-graph/AssetGraphAssetSelectionInput.oss'; +import {AssetSelectionInput} from 'shared/asset-selection/input/AssetSelectionInput.oss'; import {useAssetSelectionState} from 'shared/asset-selection/useAssetSelectionState.oss'; import {FilterableAssetDefinition} from 'shared/assets/useAssetDefinitionFilterState.oss'; -import {AssetSelectionInput} from './AssetSelectionInput'; import {featureEnabled} from '../../app/Flags'; import {useAssetSelectionFiltering} from '../useAssetSelectionFiltering'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/util.ts b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/util.ts index 537b59ede7c67..f8da53df8a18f 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/util.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/input/util.ts @@ -1,11 +1,14 @@ +import {IconName} from '@dagster-io/ui-components'; + import {assertUnreachable} from '../../app/Util'; import {AssetGraphQueryItem} from '../../asset-graph/useAssetGraphData'; import {isKindTag} from '../../graph/KindTags'; +import {weakMapMemoize} from '../../util/weakMapMemoize'; import {buildRepoPathForHuman} from '../../workspace/buildRepoAddress'; export const getAttributesMap = (assets: AssetGraphQueryItem[]) => { const assetNamesSet: Set = new Set(); - const tagNamesSet: Set = new Set(); + const tagNamesSet: Set<{key: string; value: string}> = new Set(); const ownersSet: Set = new Set(); const groupsSet: Set = new Set(); const kindsSet: Set = new Set(); @@ -17,13 +20,7 @@ export const getAttributesMap = (assets: AssetGraphQueryItem[]) => { if (isKindTag(tag)) { return; } - if (tag.key && tag.value) { - // We add quotes around the equal sign here because the auto-complete suggestion already wraps the entire value in quotes. - // So wer end up with tag:"key"="value" as the final suggestion - tagNamesSet.add(`${tag.key}"="${tag.value}`); - } else { - tagNamesSet.add(tag.key); - } + tagNamesSet.add(memoizedTag(tag.key, tag.value)); }); asset.node.owners.forEach((owner) => { switch (owner.__typename) { @@ -66,3 +63,20 @@ export const getAttributesMap = (assets: AssetGraphQueryItem[]) => { code_location: codeLocations, }; }; + +const memoizedTag = weakMapMemoize((key: string, value: string) => ({ + key, + value, +})); + +export type Attribute = 'kind' | 'code_location' | 'group' | 'owner' | 'tag' | 'status' | 'key'; + +export const attributeToIcon: Record = { + key: 'magnify_glass', + kind: 'compute_kind', + code_location: 'code_location', + group: 'asset_group', + owner: 'owner', + tag: 'tag', + status: 'status', +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/gantt/GanttChartSelectionInput.tsx b/js_modules/dagster-ui/packages/ui-core/src/gantt/GanttChartSelectionInput.tsx index 63b769ccffcca..19fb40a5495de 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/gantt/GanttChartSelectionInput.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/gantt/GanttChartSelectionInput.tsx @@ -1,13 +1,11 @@ -import {useMemo} from 'react'; import styled from 'styled-components'; import {RunGraphQueryItem} from './toGraphQueryItems'; -import {NO_STATE} from '../run-selection/AntlrRunSelectionVisitor'; +import {useGanttChartSelectionAutoCompleteProvider} from './useGanttChartSelectionAutoCompleteProvider'; import {RunSelectionLexer} from '../run-selection/generated/RunSelectionLexer'; import {RunSelectionParser} from '../run-selection/generated/RunSelectionParser'; import {InputDiv, SelectionAutoCompleteInput} from '../selection/SelectionInput'; import {createSelectionLinter} from '../selection/createSelectionLinter'; -import {useSelectionInputAutoComplete} from '../selection/useSelectionInputAutoComplete'; import {weakMapMemoize} from '../util/weakMapMemoize'; export const GanttChartSelectionInput = ({ @@ -19,12 +17,11 @@ export const GanttChartSelectionInput = ({ value: string; onChange: (value: string) => void; }) => { - const useAutoComplete = useMemo(() => createAutoComplete(items), [items]); return ( ); }; - -function createAutoComplete(items: RunGraphQueryItem[]) { - return function useAutoComplete(value: string, cursor: number) { - const attributesMap = useMemo(() => { - const statuses = new Set(); - const names = new Set(); - - items.forEach((item) => { - if (item.metadata?.state) { - statuses.add(item.metadata.state); - } else { - statuses.add(NO_STATE); - } - names.add(item.name); - }); - return {name: Array.from(names), status: Array.from(statuses)}; - }, []); - - return { - autoCompleteResults: useSelectionInputAutoComplete({ - nameBase: 'name', - attributesMap, - functions: FUNCTIONS, - value, - cursor, - }), - loading: false, - }; - }; -} - const getLinter = weakMapMemoize(() => createSelectionLinter({Lexer: RunSelectionLexer, Parser: RunSelectionParser}), ); -const FUNCTIONS = ['sinks', 'roots']; - const Wrapper = styled.div` ${InputDiv} { width: 24vw; diff --git a/js_modules/dagster-ui/packages/ui-core/src/gantt/useGanttChartSelectionAutoCompleteProvider.tsx b/js_modules/dagster-ui/packages/ui-core/src/gantt/useGanttChartSelectionAutoCompleteProvider.tsx new file mode 100644 index 0000000000000..9ca7676045e9a --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/gantt/useGanttChartSelectionAutoCompleteProvider.tsx @@ -0,0 +1,58 @@ +import {useMemo} from 'react'; + +import {RunGraphQueryItem} from './toGraphQueryItems'; +import {NO_STATE} from '../run-selection/AntlrRunSelectionVisitor'; +import {createSelectionAutoComplete} from '../selection/SelectionAutoComplete'; +import { + SelectionAutoCompleteProvider, + createProvider, +} from '../selection/SelectionAutoCompleteProvider'; + +export function useGanttChartSelectionAutoCompleteProvider( + items: RunGraphQueryItem[], +): SelectionAutoCompleteProvider { + const attributesMap = useMemo(() => { + const statuses = new Set(); + const names = new Set(); + + items.forEach((item) => { + if (item.metadata?.state) { + statuses.add(item.metadata.state); + } else { + statuses.add(NO_STATE); + } + names.add(item.name); + }); + return {name: Array.from(names), status: Array.from(statuses)}; + }, [items]); + + const baseProvider = useMemo( + () => + createProvider({ + attributesMap, + primaryAttributeKey: 'name', + attributeToIcon: iconMap, + }), + [attributesMap], + ); + const selectionHint = useMemo(() => createSelectionAutoComplete(baseProvider), [baseProvider]); + + return { + ...baseProvider, + useAutoComplete: ({line, cursorIndex}) => { + const autoCompleteResults = useMemo( + () => selectionHint(line, cursorIndex), + [line, cursorIndex], + ); + return { + autoCompleteResults, + loading: false, + }; + }, + }; +} + +export const iconMap = { + name: 'magnify_glass', + status: 'status', +} as const; diff --git a/js_modules/dagster-ui/packages/ui-core/src/pipelines/OpGraphSelectionInput.tsx b/js_modules/dagster-ui/packages/ui-core/src/pipelines/OpGraphSelectionInput.tsx index 817ef0ebf46f0..83cc911503acb 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/pipelines/OpGraphSelectionInput.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/pipelines/OpGraphSelectionInput.tsx @@ -1,12 +1,11 @@ -import {useMemo} from 'react'; import styled from 'styled-components'; +import {useOpGraphSelectionAutoCompleteProvider} from './useOpGraphSelectionAutoCompleteProvider'; import {GraphQueryItem} from '../app/GraphQueryImpl'; import {OpSelectionLexer} from '../op-selection/generated/OpSelectionLexer'; import {OpSelectionParser} from '../op-selection/generated/OpSelectionParser'; import {InputDiv, SelectionAutoCompleteInput} from '../selection/SelectionInput'; import {createSelectionLinter} from '../selection/createSelectionLinter'; -import {useSelectionInputAutoComplete} from '../selection/useSelectionInputAutoComplete'; import {weakMapMemoize} from '../util/weakMapMemoize'; export const OpGraphSelectionInput = ({ @@ -18,12 +17,11 @@ export const OpGraphSelectionInput = ({ value: string; onChange: (value: string) => void; }) => { - const useAutoComplete = useMemo(() => createAutoComplete(items), [items]); return ( { - const names = new Set(); - items.forEach((item) => { - names.add(item.name); - }); - return {name: Array.from(names)}; - }, []); - - return { - autoCompleteResults: useSelectionInputAutoComplete({ - nameBase: 'name', - attributesMap, - functions: FUNCTIONS, - value, - cursor, - }), - loading: false, - }; - }; -} - const getLinter = weakMapMemoize(() => createSelectionLinter({Lexer: OpSelectionLexer, Parser: OpSelectionParser}), ); -const FUNCTIONS = ['sinks', 'roots']; - const Wrapper = styled.div` ${InputDiv} { width: 24vw; diff --git a/js_modules/dagster-ui/packages/ui-core/src/pipelines/useOpGraphSelectionAutoCompleteProvider.tsx b/js_modules/dagster-ui/packages/ui-core/src/pipelines/useOpGraphSelectionAutoCompleteProvider.tsx new file mode 100644 index 0000000000000..1dbc7ef999d7c --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/pipelines/useOpGraphSelectionAutoCompleteProvider.tsx @@ -0,0 +1,49 @@ +import {useMemo} from 'react'; + +import {GraphQueryItem} from '../app/GraphQueryImpl'; +import {iconMap} from '../gantt/useGanttChartSelectionAutoCompleteProvider'; +import {createSelectionAutoComplete} from '../selection/SelectionAutoComplete'; +import { + SelectionAutoCompleteProvider, + createProvider, +} from '../selection/SelectionAutoCompleteProvider'; + +export const useOpGraphSelectionAutoCompleteProvider = ( + items: GraphQueryItem[], +): SelectionAutoCompleteProvider => { + const attributesMap = useMemo(() => { + const names = new Set(); + items.forEach((item) => { + names.add(item.name); + }); + return {name: Array.from(names)}; + }, [items]); + + const baseProvider = useMemo( + () => + createProvider({ + attributesMap, + primaryAttributeKey: 'name', + attributeToIcon: iconMap, + }), + [attributesMap], + ); + const selectionHint = useMemo(() => createSelectionAutoComplete(baseProvider), [baseProvider]); + + return useMemo( + () => ({ + ...baseProvider, + useAutoComplete: ({line, cursorIndex}) => { + const autoCompleteResults = useMemo( + () => selectionHint(line, cursorIndex), + [line, cursorIndex], + ); + return { + autoCompleteResults, + loading: false, + }; + }, + }), + [baseProvider, selectionHint], + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoComplete.ts b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoComplete.ts index 82be71a2409bc..6d3516887fb3d 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoComplete.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoComplete.ts @@ -1,17 +1,15 @@ +import {SelectionAutoCompleteProvider} from './SelectionAutoCompleteProvider'; import {SelectionAutoCompleteVisitor} from './SelectionAutoCompleteVisitor'; import {parseInput} from './SelectionInputParser'; -export function generateAutocompleteResults, N extends keyof T>({ - nameBase: _nameBase, - attributesMap, - functions, -}: { - nameBase: N; - attributesMap: T; - functions: string[]; -}) { - const nameBase = _nameBase as string; - +export function createSelectionAutoComplete({ + getAttributeResultsMatchingQuery, + getAttributeValueResultsMatchingQuery, + getFunctionResultsMatchingQuery, + getSubstringResultMatchingQuery, + getAttributeValueIncludeAttributeResultsMatchingQuery, + createOperatorSuggestion, +}: Omit) { return function (line: string, actualCursorIndex: number) { const {parseTrees} = parseInput(line); @@ -21,31 +19,36 @@ export function generateAutocompleteResults, if (!parseTrees.length) { // Special case empty string to add unmatched value results visitorWithAutoComplete = new SelectionAutoCompleteVisitor({ - attributesMap, - functions, - nameBase, line, cursorIndex: actualCursorIndex, + getAttributeResultsMatchingQuery, + getAttributeValueResultsMatchingQuery, + getAttributeValueIncludeAttributeResultsMatchingQuery, + getFunctionResultsMatchingQuery, + getSubstringResultMatchingQuery, + createOperatorSuggestion, }); - start = actualCursorIndex; visitorWithAutoComplete.addUnmatchedValueResults(''); } else { for (const {tree, line} of parseTrees) { const cursorIndex = actualCursorIndex - start; - const visitor = new SelectionAutoCompleteVisitor({ - attributesMap, - functions, - nameBase, - line, - cursorIndex, - }); - visitor.visit(tree); - const length = line.length; - if (cursorIndex <= length) { + + if (cursorIndex <= line.length) { + const visitor = new SelectionAutoCompleteVisitor({ + line, + cursorIndex, + getAttributeResultsMatchingQuery, + getAttributeValueResultsMatchingQuery, + getAttributeValueIncludeAttributeResultsMatchingQuery, + getFunctionResultsMatchingQuery, + getSubstringResultMatchingQuery, + createOperatorSuggestion, + }); + tree.accept(visitor); visitorWithAutoComplete = visitor; break; } - start += length; + start += line.length; } } if (visitorWithAutoComplete) { diff --git a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteProvider.tsx b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteProvider.tsx new file mode 100644 index 0000000000000..c25c55e8306fe --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteProvider.tsx @@ -0,0 +1,437 @@ +import {BodySmall, Box, Colors, Icon, IconName, MonoSmall} from '@dagster-io/ui-components'; +import React from 'react'; + +import {assertUnreachable} from '../app/Util'; + +export interface SelectionAutoCompleteProvider { + /** + * Retrieves a list of attributes that match the provided query string. + * + * @param query - The search string to match attribute names against. + * @param textCallback - An optional callback to transform the display text of each result. Used to insert spaces or double quotes if necessary depending on surrounding context + * @returns An array of attributes that match the query. + */ + getAttributeResultsMatchingQuery: (prop: { + query: string; + textCallback?: (value: string) => string; + }) => Suggestion[]; + + /** + * Retrieves a list of attribute values for a specific attribute that match the provided query string. + * + * @param attribute - The name of the attribute to search within. + * @param query - The search string to match attribute values against. + * @param textCallback - An optional callback to transform the display text of each result. Used to insert spaces or double quotes if necessary depending on surrounding context + * @returns An array of attribute values that match the query for the specified attribute. + */ + getAttributeValueResultsMatchingQuery: (prop: { + attribute: string; + query: string; + textCallback?: (value: string) => string; + }) => Array; + + /** + * Retrieves a list of function results that match the provided query string. + * + * @param query - The search string to match function names against. + * @param textCallback - An optional callback to transform the display text of each result. Used to insert spaces or double quotes if necessary depending on surrounding context + * @param options - Optional settings + * @returns An array of functions that match the query. + */ + getFunctionResultsMatchingQuery: (prop: { + query: string; + textCallback?: (value: string) => string; + options?: { + // If this is true then the result should be returned with parenthesis. (eg. "foo()" instead of "foo") + // This will be true if there aren't any parenthesis already present. + includeParenthesis?: boolean; + }; + }) => Suggestion[]; + + /** + * Retrieves a single substring result that matches the provided query string. + * + * @param query - The search string to match substrings against. + * @param textCallback - An optional callback to transform the display text of each result. Used to insert spaces or double quotes if necessary depending on surrounding context + * @returns A single substring result that matches the query. + */ + getSubstringResultMatchingQuery: (prop: { + query: string; + textCallback?: (value: string) => string; + }) => Suggestion; + + /** + * Retrieves a list of attribute values, including their corresponding attribute names, that match the provided query string. + * + * @param query - The search string to match attribute values against. + * @param textCallback - An optional callback to transform the display text of each result. Used to insert spaces or double quotes if necessary depending on surrounding context + * @returns An array of attribute values along with their attribute names that match the query. + */ + getAttributeValueIncludeAttributeResultsMatchingQuery: (prop: { + query: string; + textCallback?: (value: string) => string; + }) => Array; + + createOperatorSuggestion: (prop: { + text: string; + displayText: string; + type: OperatorType; + }) => Suggestion; + + useAutoComplete: (prop: {line: string; cursorIndex: number}) => { + autoCompleteResults: { + from: number; + to: number; + list: Array; + }; + loading: boolean; + }; +} + +export type Suggestion = { + text: string; + jsx: React.ReactNode; +}; + +type OperatorType = 'and' | 'or' | 'not' | 'parenthesis' | 'up-traversal' | 'down-traversal'; + +const operatorToIconAndLabel: Record = { + and: { + icon: 'curly_braces', + label: 'And', + }, + or: { + icon: 'curly_braces', + label: 'Or', + }, + not: { + icon: 'curly_braces', + label: 'Not', + }, + parenthesis: { + icon: 'curly_braces', + label: 'Parenthesis', + }, + 'up-traversal': { + icon: 'curly_braces', + label: 'Include upstream dependencies', + }, + 'down-traversal': { + icon: 'curly_braces', + label: 'Include downstream dependencies', + }, +}; + +export const Operator = ({displayText, type}: {displayText: string; type: OperatorType}) => { + const {icon, label} = operatorToIconAndLabel[type]; + return ; +}; + +export const AttributeValueTagSuggestion = ({ + key, + value, +}: { + key: string; + value?: string | null | undefined; +}) => { + const valueText = value ? `${key}=${value}` : key; + return ; +}; + +export const AttributeWithStringValueSuggestionJSX = ({ + icon, + attributeName, + value, +}: { + icon?: IconName | null; + attributeName: string; + value: string; +}) => { + return ( + + {attributeName}: + {value} + + } + /> + ); +}; + +export const AttributeWithTagValueSuggestionJSX = ({ + icon, + attributeName, + tag, +}: { + icon?: IconName | null; + attributeName: string; + tag: { + key: string; + value?: string | null | undefined; + }; +}) => { + return ( + + {attributeName}: + {tag.value ? `${tag.key}=${tag.value}` : tag.key} + + } + /> + ); +}; + +export const FunctionSuggestionJSX = ({ + functionName, + includeParenthesis, +}: { + functionName: string; + includeParenthesis?: boolean; +}) => { + const fn = includeParenthesis ? `${functionName}()` : functionName; + return ; +}; + +export const SuggestionJSXBase = ({ + label, + icon, + rightLabel, +}: { + label: React.ReactNode; + icon?: IconName | null; + rightLabel?: React.ReactNode; +}) => { + return ( + + + {icon ? : null} + {label} + + {rightLabel ? {rightLabel} : null} + + ); +}; + +export const createProvider = < + TAttributeMap extends {[key: string]: string[] | {key: string; value?: string}[]}, + TPrimaryAttributeKey extends keyof TAttributeMap, +>({ + attributeToIcon, + primaryAttributeKey, + attributesMap, +}: { + attributeToIcon: Record; + primaryAttributeKey: TPrimaryAttributeKey; + attributesMap: TAttributeMap; +}): Omit => { + const functions = ['sinks', 'roots'] as const; + function doesValueIncludeQuery({ + value, + query, + }: { + value: TAttributeMap[keyof TAttributeMap][0]; + query: string; + }) { + if (typeof value !== 'string') { + return ( + value.key.includes(query) || + value.value?.includes(query) || + `${value.key}=${value.value ?? ''}`.includes(query) + ); + } + return value.includes(query); + } + + function createAttributeSuggestion({ + attribute, + text, + }: { + attribute: keyof TAttributeMap; + text: string; + }) { + const displayText = `${attribute as string}:`; + const icon: IconName = attributeToIcon[attribute]; + let label; + switch (attribute) { + case primaryAttributeKey as string: + label = 'Exact match'; + break; + default: + label = (attribute as string).replace(/_/g, ' '); + label = label[0]!.toUpperCase() + label.slice(1); + } + return { + text, + jsx: , + }; + } + + function createAttributeValueSuggestion({ + value, + textCallback, + }: { + value: TAttributeMap[keyof TAttributeMap][0]; + textCallback?: (text: string) => string; + }) { + if (typeof value !== 'string') { + const valueText = value.value ? `"${value.key}"="${value.value}"` : `"${value.key}"`; + return { + text: textCallback ? textCallback(valueText) : valueText, + jsx: , + }; + } + return { + text: textCallback ? textCallback(`"${value}"`) : `"${value}"`, + jsx: , + }; + } + + function createFunctionSuggestion({ + func, + text, + options, + }: { + func: (typeof functions)[number]; + text: string; + options?: {includeParenthesis?: boolean}; + }) { + const functionName = func[0]!.toUpperCase() + func.slice(1); + const displayText = options?.includeParenthesis ? `${functionName}()` : functionName; + let icon: IconName; + switch (func) { + case 'roots': + icon = 'arrow_upward'; + break; + case 'sinks': + icon = 'arrow_indent'; + break; + default: + assertUnreachable(func); + } + return { + text, + jsx: , + }; + } + + function createSubstringSuggestion({ + query, + textCallback, + }: { + query: string; + textCallback?: (text: string) => string; + }) { + const text = `key_substring:"${query}"`; + return { + text: textCallback ? textCallback(text) : text, + jsx: , + }; + } + + function createAttributeValueIncludeAttributeSuggestion({ + attribute, + value, + textCallback, + }: { + attribute: keyof TAttributeMap; + value: TAttributeMap[keyof TAttributeMap][0]; + textCallback?: (text: string) => string; + }) { + let text; + let valueText; + if (typeof value !== 'string') { + if (value.value) { + text = `${attribute as string}:"${value.key}"="${value.value}"`; + valueText = `${value.key}=${value.value}`; + } else { + text = `${attribute as string}:"${value.key}"`; + valueText = value.key; + } + } else { + text = `${attribute as string}:"${value}"`; + valueText = value; + } + return { + text: textCallback ? textCallback(text) : text, + jsx: ( + + {attribute as string}: + {valueText} + + } + /> + ), + }; + } + + return { + createOperatorSuggestion: ({type, text, displayText}) => { + return { + text, + jsx: , + }; + }, + getAttributeResultsMatchingQuery: ({query, textCallback}) => { + return Object.keys(attributesMap) + .filter((attr) => attr.startsWith(query)) + .map((attr) => + createAttributeSuggestion({ + attribute: attr, + text: textCallback ? textCallback(`${attr}:`) : `${attr}:`, + }), + ); + }, + getAttributeValueResultsMatchingQuery: ({attribute, query, textCallback}) => { + let values = attributesMap[attribute as keyof typeof attributesMap]; + if (attribute === `${primaryAttributeKey as string}_substring`) { + values = attributesMap[primaryAttributeKey]; + } + return ( + values + ?.filter((value) => doesValueIncludeQuery({value, query})) + .map((value) => + createAttributeValueSuggestion({ + value, + textCallback, + }), + ) ?? [] + ); + }, + getFunctionResultsMatchingQuery: ({query, textCallback, options}) => { + return functions + .filter((func) => func.startsWith(query)) + .map((func) => { + const value = options?.includeParenthesis ? `${func}()` : func; + return createFunctionSuggestion({ + func, + text: textCallback ? textCallback(value) : value, + options, + }); + }); + }, + getSubstringResultMatchingQuery: ({query, textCallback}) => { + return createSubstringSuggestion({query, textCallback}); + }, + getAttributeValueIncludeAttributeResultsMatchingQuery: ({query, textCallback}) => { + return Object.keys(attributesMap).flatMap((attribute) => { + return ( + attributesMap[attribute] + ?.filter((value) => doesValueIncludeQuery({value, query})) + .map((value) => + createAttributeValueIncludeAttributeSuggestion({ + attribute, + value, + textCallback, + }), + ) ?? [] + ); + }); + }, + }; +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteVisitor.ts b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteVisitor.ts index 1c337ad6f2053..d2bb2f949532b 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteVisitor.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteVisitor.ts @@ -1,6 +1,7 @@ import {ParserRuleContext} from 'antlr4ts'; import {BaseSelectionVisitor} from './BaseSelectionVisitor'; +import {SelectionAutoCompleteProvider, Suggestion} from './SelectionAutoCompleteProvider'; import { getValueNodeValue, isInsideExpressionlessParenthesizedExpression, @@ -32,81 +33,47 @@ const DEFAULT_TEXT_CALLBACK = (value: string) => value; // set to true for debug output if desired const DEBUG = false; -export type Suggestion = - | { - text: string; - displayText: string; - type: 'function' | 'logical_operator' | 'parenthesis'; - attributeName?: never; - nameBase?: never; - } - | { - text: string; - displayText: string; - type: 'attribute'; - attributeName?: string; - nameBase?: boolean; - } - | { - text: string; - displayText: string; - type: 'attribute-value'; - attributeName?: string; - nameBase?: never; - } - | { - text: string; - displayText: string; - type: 'attribute-with-value'; - attributeName?: string; - nameBase?: never; - } - | { - text: string; - displayText: string; - type: 'up-traversal' | 'down-traversal'; - attributeName?: never; - nameBase?: never; - }; - export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { - private attributesMap: Record; - private functions: string[]; - private nameBase: string; - private attributesWithNameBase: string[]; - private allAttributes: {key: string; value: string}[]; - public list: Suggestion[] = []; + private getAttributeResultsMatchingQuery: SelectionAutoCompleteProvider['getAttributeResultsMatchingQuery']; + private getAttributeValueResultsMatchingQuery: SelectionAutoCompleteProvider['getAttributeValueResultsMatchingQuery']; + private getAttributeValueIncludeAttributeResultsMatchingQuery: SelectionAutoCompleteProvider['getAttributeValueIncludeAttributeResultsMatchingQuery']; + private getFunctionResultsMatchingQuery: SelectionAutoCompleteProvider['getFunctionResultsMatchingQuery']; + private getSubstringResultMatchingQuery: SelectionAutoCompleteProvider['getSubstringResultMatchingQuery']; + private createOperatorSuggestion: SelectionAutoCompleteProvider['createOperatorSuggestion']; + + public list: Array = []; // Replacement indices from the original code public _startReplacementIndex: number; public _stopReplacementIndex: number; constructor({ - attributesMap, - functions, - nameBase, line, cursorIndex, + getAttributeResultsMatchingQuery, + getAttributeValueResultsMatchingQuery, + getFunctionResultsMatchingQuery, + getSubstringResultMatchingQuery, + getAttributeValueIncludeAttributeResultsMatchingQuery, + createOperatorSuggestion, }: { - attributesMap: Record; - functions: string[]; - nameBase: string; line: string; cursorIndex: number; + getAttributeResultsMatchingQuery: SelectionAutoCompleteProvider['getAttributeResultsMatchingQuery']; + getAttributeValueResultsMatchingQuery: SelectionAutoCompleteProvider['getAttributeValueResultsMatchingQuery']; + getAttributeValueIncludeAttributeResultsMatchingQuery: SelectionAutoCompleteProvider['getAttributeValueIncludeAttributeResultsMatchingQuery']; + getFunctionResultsMatchingQuery: SelectionAutoCompleteProvider['getFunctionResultsMatchingQuery']; + getSubstringResultMatchingQuery: SelectionAutoCompleteProvider['getSubstringResultMatchingQuery']; + createOperatorSuggestion: SelectionAutoCompleteProvider['createOperatorSuggestion']; }) { super({line, cursorIndex}); - this.attributesMap = attributesMap; - this.functions = functions; - this.nameBase = nameBase; - this.attributesWithNameBase = [ - this.nameBase, - ...Object.keys(attributesMap).filter((name) => name !== this.nameBase), - ]; - this.allAttributes = Object.keys(attributesMap).flatMap((key) => { - return ( - attributesMap[key]?.sort((a, b) => a.localeCompare(b)).map((value) => ({key, value})) ?? [] - ); - }); + this.getAttributeResultsMatchingQuery = getAttributeResultsMatchingQuery; + this.getAttributeValueResultsMatchingQuery = getAttributeValueResultsMatchingQuery; + this.getFunctionResultsMatchingQuery = getFunctionResultsMatchingQuery; + this.getSubstringResultMatchingQuery = getSubstringResultMatchingQuery; + this.getAttributeValueIncludeAttributeResultsMatchingQuery = + getAttributeValueIncludeAttributeResultsMatchingQuery; + this.createOperatorSuggestion = createOperatorSuggestion; this._startReplacementIndex = cursorIndex; this._stopReplacementIndex = cursorIndex; } @@ -144,20 +111,50 @@ export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { this.stopReplacementIndex = this.cursorIndex; this.addUnmatchedValueResults('', DEFAULT_TEXT_CALLBACK, {excludePlus: true}); if (isInsideExpressionlessParenthesizedExpression(ctx)) { - this.list.push({text: ')', displayText: ')', type: 'parenthesis' as const}); + this.list.push( + this.createOperatorSuggestion({ + text: ')', + type: 'parenthesis', + displayText: ')', + }), + ); } } } public visitUpTraversal(_ctx: UpTraversalContext) { - this.list.push({text: '()', displayText: '(', type: 'parenthesis' as const}); + this.list.push( + this.createOperatorSuggestion({ + text: '()', + type: 'parenthesis', + displayText: '(', + }), + ); } public visitDownTraversal(ctx: DownTraversalContext) { - this.list.push({text: ' and ', displayText: 'and', type: 'logical_operator' as const}); - this.list.push({text: ' or ', displayText: 'or', type: 'logical_operator' as const}); + this.list.push( + this.createOperatorSuggestion({ + text: ' and ', + type: 'and', + displayText: 'and', + }), + ); + this.list.push( + this.createOperatorSuggestion({ + text: ' or ', + type: 'or', + displayText: 'or', + }), + ); if (isInsideExpressionlessParenthesizedExpression(ctx)) { - this.list.push({text: ')', displayText: ')', type: 'parenthesis' as const}); + this.list.push( + this.createOperatorSuggestion({ + text: ')', + type: 'parenthesis', + displayText: ')', + }), + ); } } @@ -231,8 +228,8 @@ export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { this.startReplacementIndex = ctx.start.startIndex; this.stopReplacementIndex = ctx.stop!.stopIndex + 1; this.list.push( - {text: 'or', displayText: 'or', type: 'logical_operator'}, - {text: 'and', displayText: 'and', type: 'logical_operator'}, + this.createOperatorSuggestion({text: 'or', type: 'or', displayText: 'or'}), + this.createOperatorSuggestion({text: 'and', type: 'and', displayText: 'and'}), ); } } @@ -244,8 +241,8 @@ export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { this.startReplacementIndex = ctx.start.startIndex; this.stopReplacementIndex = ctx.stop!.stopIndex + 1; this.list.push( - {text: 'and', displayText: 'and', type: 'logical_operator'}, - {text: 'or', displayText: 'or', type: 'logical_operator'}, + this.createOperatorSuggestion({text: 'and', type: 'and', displayText: 'and'}), + this.createOperatorSuggestion({text: 'or', type: 'or', displayText: 'or'}), ); } } @@ -329,18 +326,10 @@ export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { textCallback: (value: string) => string = DEFAULT_TEXT_CALLBACK, ) { this.list.push( - ...this.attributesWithNameBase - .filter((attr) => attr.startsWith(value.trim())) - .map((val) => { - const suggestionValue = `${val}:`; - return { - text: textCallback(suggestionValue), - displayText: suggestionValue, - type: 'attribute' as const, - attributeName: val, - nameBase: val === this.nameBase || val === `${this.nameBase}_substring`, - }; - }), + ...this.getAttributeResultsMatchingQuery({ + query: value.trim(), + textCallback, + }), ); } @@ -350,16 +339,11 @@ export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { includeParenthesis: boolean = false, ) { this.list.push( - ...this.functions - .filter((fn) => fn.startsWith(value.trim())) - .map((val) => { - const suggestionValue = `${val}${includeParenthesis ? '()' : ''}`; - return { - text: textCallback(suggestionValue), - displayText: suggestionValue, - type: 'function' as const, - }; - }), + ...this.getFunctionResultsMatchingQuery({ + query: value.trim(), + textCallback, + options: {includeParenthesis}, + }), ); } @@ -370,51 +354,51 @@ export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { ) { const value = _value.trim(); if (value) { - const substringMatchDisplayText = `${this.nameBase}_substring:${removeQuotesFromString(value)}`; - const substringMatchText = `${this.nameBase}_substring:"${removeQuotesFromString(value)}"`; - this.list.push({ - text: textCallback(substringMatchText), - displayText: substringMatchDisplayText, - type: 'attribute-with-value' as const, - attributeName: `${this.nameBase}_substring`, - }); + this.list.push( + this.getSubstringResultMatchingQuery({ + query: value, + textCallback, + }), + ); } this.addAttributeResults(value, textCallback); this.addFunctionResults(value, textCallback, true); if (value) { - this.allAttributes.forEach((attribute) => { - if (attribute.value.includes(value.toLowerCase())) { - this.list.push({ - text: textCallback(`${attribute.key}:"${attribute.value}"`), - displayText: `${attribute.key}:${attribute.value}`, - type: 'attribute-with-value' as const, - attributeName: attribute.key, - }); - } - }); + this.list.push( + ...this.getAttributeValueIncludeAttributeResultsMatchingQuery({ + query: value, + textCallback, + }), + ); } if (!options.excludeNot && 'not'.startsWith(value)) { - this.list.push({ - text: textCallback('not '), - displayText: 'not', - type: 'logical_operator' as const, - }); + this.list.push( + this.createOperatorSuggestion({ + text: textCallback('not '), + type: 'not', + displayText: 'not', + }), + ); } if (value === '') { if (!options.excludePlus) { - this.list.push({ - text: textCallback('+'), - displayText: '+', - type: 'up-traversal' as const, - }); + this.list.push( + this.createOperatorSuggestion({ + text: textCallback('+'), + type: 'up-traversal', + displayText: '+', + }), + ); } - this.list.push({ - text: textCallback('()'), - displayText: '(', - type: 'parenthesis' as const, - }); + this.list.push( + this.createOperatorSuggestion({ + text: textCallback('()'), + type: 'parenthesis', + displayText: '(', + }), + ); } } @@ -423,21 +407,14 @@ export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { value: string, textCallback: (value: string) => string = DEFAULT_TEXT_CALLBACK, ) { - let possibleValues = this.attributesMap[attributeKey] ?? []; - if (attributeKey === `${this.nameBase}_substring`) { - possibleValues = this.attributesMap[this.nameBase]!; - } const unquotedValue = removeQuotesFromString(value); - possibleValues.forEach((attributeValue) => { - if (attributeValue.includes(unquotedValue)) { - this.list.push({ - text: textCallback(`"${attributeValue}"`), - displayText: attributeValue, - type: 'attribute-value' as const, - attributeName: attributeKey, - }); - } - }); + this.list.push( + ...this.getAttributeValueResultsMatchingQuery({ + attribute: attributeKey, + query: unquotedValue, + textCallback, + }), + ); } private addAfterExpressionResults( @@ -447,16 +424,20 @@ export class SelectionAutoCompleteVisitor extends BaseSelectionVisitor { } = {}, ) { this.list.push( - {text: ' and ', displayText: 'and', type: 'logical_operator' as const}, - {text: ' or ', displayText: 'or', type: 'logical_operator' as const}, + this.createOperatorSuggestion({text: ' and ', type: 'and', displayText: 'and'}), + this.createOperatorSuggestion({text: ' or ', type: 'or', displayText: 'or'}), ); if (!options.excludePlus) { - this.list.push({text: '+', displayText: '+', type: 'down-traversal' as const}); + this.list.push( + this.createOperatorSuggestion({text: '+', type: 'down-traversal', displayText: '+'}), + ); } if (isInsideExpressionlessParenthesizedExpression(ctx)) { - this.list.push({text: ')', displayText: ')', type: 'parenthesis' as const}); + this.list.push( + this.createOperatorSuggestion({text: ')', type: 'parenthesis', displayText: ')'}), + ); } } } diff --git a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionInput.tsx b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionInput.tsx index dfaf029a4ef1b..6bf12f7cb7dee 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionInput.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionInput.tsx @@ -6,7 +6,7 @@ import debounce from 'lodash/debounce'; import React, {KeyboardEvent, useCallback, useLayoutEffect, useMemo, useRef, useState} from 'react'; import styled from 'styled-components'; -import {Suggestion} from './SelectionAutoCompleteVisitor'; +import {SelectionAutoCompleteProvider} from './SelectionAutoCompleteProvider'; import {SelectionInputAutoCompleteResults} from './SelectionInputAutoCompleteResults'; import { SelectionAutoCompleteInputCSS, @@ -28,17 +28,7 @@ type SelectionAutoCompleteInputProps = { linter: Linter; value: string; onChange: (value: string) => void; - useAutoComplete: ( - selection: string, - cursor: number, - ) => { - autoCompleteResults: { - list: Suggestion[]; - from: number; - to: number; - }; - loading: boolean; - }; + SelectionAutoCompleteProvider: SelectionAutoCompleteProvider; }; export const SelectionAutoCompleteInput = ({ @@ -47,8 +37,10 @@ export const SelectionAutoCompleteInput = ({ placeholder, onChange, linter, - useAutoComplete, + SelectionAutoCompleteProvider, }: SelectionAutoCompleteInputProps) => { + const {useAutoComplete} = SelectionAutoCompleteProvider; + const trackEvent = useTrackEvent(); const trackSelection = useMemo(() => { @@ -81,7 +73,10 @@ export const SelectionAutoCompleteInput = ({ const [cursorPosition, setCursorPosition] = useState(0); const [innerValue, setInnerValue] = useState(value); - const {autoCompleteResults, loading} = useAutoComplete(innerValue, cursorPosition); + const {autoCompleteResults, loading} = useAutoComplete({ + line: innerValue, + cursorIndex: cursorPosition, + }); const hintContainerRef = useRef(null); @@ -92,7 +87,7 @@ export const SelectionAutoCompleteInput = ({ useDangerousRenderEffect(() => { // Rather then using a useEffect + setState (extra render), we just set the current value directly selectedIndexRef.current = 0; - if (!autoCompleteResults?.list.length && !loading) { + if (!autoCompleteResults.list.length && !loading) { showResults.current = false; } }, [autoCompleteResults, loading]); @@ -218,7 +213,7 @@ export const SelectionAutoCompleteInput = ({ const selectedItem = autoCompleteResults?.list[selectedIndexRef.current]; const onSelect = useCallback( - (suggestion: Suggestion) => { + (suggestion: {text: string}) => { if (autoCompleteResults && suggestion && cmInstance.current) { const editor = cmInstance.current; editor.replaceRange( diff --git a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionInputAutoCompleteResults.tsx b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionInputAutoCompleteResults.tsx index 62750894baada..7979e20dfb9a9 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionInputAutoCompleteResults.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionInputAutoCompleteResults.tsx @@ -4,18 +4,16 @@ import { Colors, Container, Icon, - IconName, Inner, Menu, MenuItem, - MonoSmall, Row, } from '@dagster-io/ui-components'; import {useVirtualizer} from '@tanstack/react-virtual'; import React, {useEffect} from 'react'; import styled from 'styled-components'; -import {Suggestion} from './SelectionAutoCompleteVisitor'; +import {Suggestion} from './SelectionAutoCompleteProvider'; import {IndeterminateLoadingBar} from '../ui/IndeterminateLoadingBar'; type SelectionInputAutoCompleteResultsProps = { @@ -31,29 +29,6 @@ type SelectionInputAutoCompleteResultsProps = { loading?: boolean; }; -type Attribute = - | 'column:' - | 'table_name:' - | 'column_tag:' - | 'kind:' - | 'code_location:' - | 'group:' - | 'owner:' - | 'tag:' - | 'status:'; - -const attributeToIcon: {[key in Attribute]: IconName} = { - 'column:': 'view_column', - 'table_name:': 'database', - 'column_tag:': 'label', - 'kind:': 'compute_kind', - 'code_location:': 'code_location', - 'group:': 'asset_group', - 'owner:': 'owner', - 'tag:': 'tag', - 'status:': 'status', -}; - export const SelectionInputAutoCompleteResults = React.memo( ({ results, @@ -98,7 +73,7 @@ export const SelectionInputAutoCompleteResults = React.memo(
} + text={result.jsx} active={index === selectedIndex} onClick={() => onSelect(result)} onMouseMove={() => setSelectedIndex({current: index})} @@ -118,7 +93,7 @@ export const SelectionInputAutoCompleteResults = React.memo( justifyContent: 'space-between', gap: 32, }} - padding={{vertical: 8, horizontal: 12}} + padding={{vertical: 4, horizontal: 12}} style={{color: Colors.textLight(), backgroundColor: Colors.backgroundGray()}} > @@ -156,79 +131,6 @@ export const SelectionInputAutoCompleteResults = React.memo( }, ); -const SuggestionItem = ({suggestion}: {suggestion: Suggestion}) => { - let label; - let icon: IconName | null = null; - let value: string | null = suggestion.displayText; - if (suggestion.nameBase) { - if (suggestion.text.endsWith('_substring:')) { - icon = 'magnify_glass_checked'; - label = 'Contains match'; - } else { - icon = 'magnify_glass'; - label = 'Exact match'; - } - } else if (suggestion.type === 'down-traversal' || suggestion.type === 'up-traversal') { - icon = 'curly_braces'; - label = - suggestion.type === 'down-traversal' - ? 'Include downstream dependencies' - : 'Include upstream dependencies'; - } else if (suggestion.type === 'logical_operator') { - icon = 'curly_braces'; - label = suggestion.displayText.toUpperCase(); - } else if (suggestion.type === 'parenthesis') { - icon = 'curly_braces'; - label = 'Parenthesis'; - value = suggestion.text; - } else if (suggestion.type === 'function') { - if (suggestion.displayText === 'roots()') { - label = 'Roots'; - icon = 'arrow_upward'; - } else if (suggestion.displayText === 'sinks()') { - label = 'Sinks'; - icon = 'arrow_indent'; - } - } else if (suggestion.type === 'attribute') { - label = suggestion.displayText.replace(':', '').replace('_', ' '); - label = label.charAt(0).toUpperCase() + label.slice(1); - - icon = attributeToIcon[suggestion.displayText as Attribute]; - } else if (suggestion.type === 'attribute-with-value') { - // hacky approach to extract the substring filter for now..., upcoming refactor will have more robust logic. - const substringMatch = /^([a-zA-Z]+)_substring:(.+)$/.exec(suggestion.text); - if (substringMatch) { - const nameBase = substringMatch[1]!; - label = `${nameBase[0]!.toUpperCase() + nameBase.slice(1)} contains ${substringMatch[2]}`; - icon = 'magnify_glass_checked'; - value = suggestion.displayText; - } else { - const firstColon = suggestion.displayText.indexOf(':'); - const attributeKey = suggestion.displayText.slice(0, firstColon); - const attributeValue = suggestion.displayText.slice(firstColon + 1); - label = ( - - {attributeKey}: - {attributeValue} - - ); - value = null; - } - } else if (suggestion.type === 'attribute-value') { - label = suggestion.displayText; - value = null; - } - return ( - - - {icon ? : null} - {label} - - {value} - - ); -}; - const KeyHintWrapper = styled.div` border-radius: 8px; padding: 4px; diff --git a/js_modules/dagster-ui/packages/ui-core/src/selection/__tests__/SelectionAutoComplete.test.ts b/js_modules/dagster-ui/packages/ui-core/src/selection/__tests__/SelectionAutoComplete.test.ts index 9c11727cf9d3d..a6b3a297a558e 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/selection/__tests__/SelectionAutoComplete.test.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/selection/__tests__/SelectionAutoComplete.test.ts @@ -1,22 +1,32 @@ -import {generateAutocompleteResults} from '../SelectionAutoComplete'; +import {createSelectionAutoComplete} from '../SelectionAutoComplete'; +import {createProvider} from '../SelectionAutoCompleteProvider'; describe('createAssetSelectionHint', () => { - const selectionHint = generateAutocompleteResults({ - nameBase: 'key', - attributesMap: { - key: ['asset1', 'asset2', 'asset3'], - tag: ['tag1', 'tag2', 'tag3'], - owner: ['marco@dagsterlabs.com', 'team:frontend'], - group: ['group1', 'group2'], - kind: ['kind1', 'kind2'], - code_location: ['repo1@location1', 'repo2@location2'], + const attributesMap = { + key: ['asset1', 'asset2', 'asset3'], + tag: ['tag1', 'tag2', 'tag3'], + owner: ['marco@dagsterlabs.com', 'team:frontend'], + group: ['group1', 'group2'], + kind: ['kind1', 'kind2'], + code_location: ['repo1@location1', 'repo2@location2'], + }; + const provider = createProvider({ + attributesMap, + primaryAttributeKey: 'key', + attributeToIcon: { + key: 'magnify_glass', + tag: 'magnify_glass', + owner: 'magnify_glass', + group: 'magnify_glass', + kind: 'magnify_glass', + code_location: 'magnify_glass', }, - functions: ['sinks', 'roots'], }); + const selectionHint = createSelectionAutoComplete(provider); function testAutocomplete(testString: string) { const cursorIndex = testString.indexOf('|'); - const string = testString.split('|').join(''); + const string = testString.replace('|', ''); const hints = selectionHint(string, cursorIndex); @@ -27,32 +37,19 @@ describe('createAssetSelectionHint', () => { }; } - beforeEach(() => { - jest.clearAllMocks(); - }); - it('should suggest asset names after typing key_substring:', () => { // cursorIndex 14 expect(testAutocomplete('key_substring:|')).toEqual({ list: [ - { + expect.objectContaining({ text: '"asset1"', - displayText: 'asset1', - type: 'attribute-value', - attributeName: 'key_substring', - }, - { + }), + expect.objectContaining({ text: '"asset2"', - displayText: 'asset2', - type: 'attribute-value', - attributeName: 'key_substring', - }, - { + }), + expect.objectContaining({ text: '"asset3"', - displayText: 'asset3', - type: 'attribute-value', - attributeName: 'key_substring', - }, + }), ], from: 14, // cursor location to: 14, // cursor location @@ -62,18 +59,12 @@ describe('createAssetSelectionHint', () => { it('should suggest owners after typing owner:', () => { expect(testAutocomplete('owner:|')).toEqual({ list: [ - { + expect.objectContaining({ text: '"marco@dagsterlabs.com"', - displayText: 'marco@dagsterlabs.com', - type: 'attribute-value', - attributeName: 'owner', - }, - { + }), + expect.objectContaining({ text: '"team:frontend"', - displayText: 'team:frontend', - type: 'attribute-value', - attributeName: 'owner', - }, + }), ], from: 6, // cursor location to: 6, // cursor location @@ -83,9 +74,15 @@ describe('createAssetSelectionHint', () => { it('should suggest tag names after typing tag:', () => { expect(testAutocomplete('tag:|')).toEqual({ list: [ - {text: '"tag1"', displayText: 'tag1', type: 'attribute-value', attributeName: 'tag'}, - {text: '"tag2"', displayText: 'tag2', type: 'attribute-value', attributeName: 'tag'}, - {text: '"tag3"', displayText: 'tag3', type: 'attribute-value', attributeName: 'tag'}, + expect.objectContaining({ + text: '"tag1"', + }), + expect.objectContaining({ + text: '"tag2"', + }), + expect.objectContaining({ + text: '"tag3"', + }), ], from: 4, // cursor location to: 4, // cursor location @@ -93,9 +90,15 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('tag:"|"')).toEqual({ list: [ - {text: '"tag1"', displayText: 'tag1', type: 'attribute-value', attributeName: 'tag'}, - {text: '"tag2"', displayText: 'tag2', type: 'attribute-value', attributeName: 'tag'}, - {text: '"tag3"', displayText: 'tag3', type: 'attribute-value', attributeName: 'tag'}, + expect.objectContaining({ + text: '"tag1"', + }), + expect.objectContaining({ + text: '"tag2"', + }), + expect.objectContaining({ + text: '"tag3"', + }), ], from: 4, // cursor location to: 6, // cursor location @@ -105,9 +108,15 @@ describe('createAssetSelectionHint', () => { it('should suggest logical operators after an expression', () => { expect(testAutocomplete('key:"asset1" |')).toEqual({ list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'down-traversal'}, + expect.objectContaining({ + text: ' and ', + }), + expect.objectContaining({ + text: ' or ', + }), + expect.objectContaining({ + text: '+', + }), ], from: 13, // cursor location to: 13, // cursor location @@ -115,9 +124,15 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('key:"asset1"|')).toEqual({ list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'down-traversal'}, + expect.objectContaining({ + text: ' and ', + }), + expect.objectContaining({ + text: ' or ', + }), + expect.objectContaining({ + text: '+', + }), ], from: 12, // cursor location to: 12, // cursor location @@ -127,12 +142,9 @@ describe('createAssetSelectionHint', () => { it('should filter suggestions based on partial input', () => { expect(testAutocomplete('owner:marco|')).toEqual({ list: [ - { + expect.objectContaining({ text: '"marco@dagsterlabs.com"', - displayText: 'marco@dagsterlabs.com', - type: 'attribute-value', - attributeName: 'owner', - }, + }), ], from: 6, // start of value "marco" to: 11, // end of value @@ -143,53 +155,40 @@ describe('createAssetSelectionHint', () => { // empty case expect(testAutocomplete('|')).toEqual({ list: [ - { - displayText: 'key:', + expect.objectContaining({ text: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - displayText: 'kind:', + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {displayText: 'sinks()', text: 'sinks()', type: 'function'}, - {displayText: 'roots()', text: 'roots()', type: 'function'}, - {displayText: 'not', text: 'not ', type: 'logical_operator'}, - {displayText: '+', text: '+', type: 'up-traversal'}, - {displayText: '(', text: '()', type: 'parenthesis'}, + }), + expect.objectContaining({ + text: 'sinks()', + }), + expect.objectContaining({ + text: 'roots()', + }), + expect.objectContaining({ + text: 'not ', + }), + expect.objectContaining({ + text: '+', + }), + + expect.objectContaining({ + text: '()', + }), ], from: 0, // cursor location to: 0, // cursor location @@ -198,55 +197,30 @@ describe('createAssetSelectionHint', () => { // filtered case expect(testAutocomplete('o|')).toEqual({ list: [ - { - displayText: 'key_substring:o', + expect.objectContaining({ text: 'key_substring:"o"', - type: 'attribute-with-value', - attributeName: 'key_substring', - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'owner:marco@dagsterlabs.com', + }), + expect.objectContaining({ text: 'owner:"marco@dagsterlabs.com"', - type: 'attribute-with-value', - attributeName: 'owner', - }, - { - displayText: 'owner:team:frontend', + }), + expect.objectContaining({ text: 'owner:"team:frontend"', - type: 'attribute-with-value', - attributeName: 'owner', - }, - { - displayText: 'group:group1', + }), + expect.objectContaining({ text: 'group:"group1"', - type: 'attribute-with-value', - attributeName: 'group', - }, - { - displayText: 'group:group2', + }), + expect.objectContaining({ text: 'group:"group2"', - type: 'attribute-with-value', - attributeName: 'group', - }, - { - displayText: 'code_location:repo1@location1', + }), + expect.objectContaining({ text: 'code_location:"repo1@location1"', - type: 'attribute-with-value', - attributeName: 'code_location', - }, - { - displayText: 'code_location:repo2@location2', + }), + expect.objectContaining({ text: 'code_location:"repo2@location2"', - type: 'attribute-with-value', - attributeName: 'code_location', - }, + }), ], from: 0, // start of input to: 1, // cursor location @@ -256,51 +230,27 @@ describe('createAssetSelectionHint', () => { it('should handle traversal operators correctly', () => { expect(testAutocomplete('+|')).toEqual({ list: [ - { - displayText: 'key:', + expect.objectContaining({ text: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - attributeName: 'kind', - displayText: 'kind:', - nameBase: false, + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {displayText: 'sinks()', text: 'sinks()', type: 'function'}, - {displayText: 'roots()', text: 'roots()', type: 'function'}, - {displayText: '(', text: '()', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: '()'}), ], from: 1, // cursor location to: 1, // cursor location @@ -309,8 +259,12 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('* |')).toEqual({ from: 2, list: [ - {displayText: 'and', text: ' and ', type: 'logical_operator'}, - {displayText: 'or', text: ' or ', type: 'logical_operator'}, + expect.objectContaining({ + text: ' and ', + }), + expect.objectContaining({ + text: ' or ', + }), ], to: 2, }); @@ -318,52 +272,28 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('+ |')).toEqual({ from: 2, list: [ - { - displayText: 'key:', + expect.objectContaining({ text: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - displayText: 'kind:', + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {displayText: 'sinks()', text: 'sinks()', type: 'function'}, - {displayText: 'roots()', text: 'roots()', type: 'function'}, - {displayText: 'not', text: 'not ', type: 'logical_operator'}, - {displayText: '(', text: '()', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: 'not '}), + expect.objectContaining({text: '()'}), ], to: 2, }); @@ -371,51 +301,27 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('+|')).toEqual({ from: 1, list: [ - { - displayText: 'key:', + expect.objectContaining({ text: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - attributeName: 'kind', - displayText: 'kind:', - nameBase: false, + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {displayText: 'sinks()', text: 'sinks()', type: 'function'}, - {displayText: 'roots()', text: 'roots()', type: 'function'}, - {displayText: '(', text: '()', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: '()'}), ], to: 1, }); @@ -424,18 +330,12 @@ describe('createAssetSelectionHint', () => { it('should suggest code locations after typing code_location:', () => { expect(testAutocomplete('code_location:|')).toEqual({ list: [ - { + expect.objectContaining({ text: '"repo1@location1"', - displayText: 'repo1@location1', - type: 'attribute-value', - attributeName: 'code_location', - }, - { + }), + expect.objectContaining({ text: '"repo2@location2"', - displayText: 'repo2@location2', - type: 'attribute-value', - attributeName: 'code_location', - }, + }), ], from: 14, to: 14, @@ -445,52 +345,28 @@ describe('createAssetSelectionHint', () => { it('should handle incomplete "not" expressions', () => { expect(testAutocomplete('not|')).toEqual({ list: [ - { + expect.objectContaining({ text: ' key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: ' tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: ' owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: ' group:', - displayText: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: ' kind:', - displayText: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: ' code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: ' sinks()', displayText: 'sinks()', type: 'function'}, - {text: ' roots()', displayText: 'roots()', type: 'function'}, - {text: ' +', displayText: '+', type: 'up-traversal'}, - {text: ' ()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: ' sinks()'}), + expect.objectContaining({text: ' roots()'}), + expect.objectContaining({text: ' +'}), + expect.objectContaining({text: ' ()'}), ], from: 3, // cursor location to: 3, // cursor location @@ -498,52 +374,28 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('not |')).toEqual({ list: [ - { + expect.objectContaining({ text: 'key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: 'tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'group:', - displayText: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - attributeName: 'kind', - displayText: 'kind:', - nameBase: false, + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - }, - { + }), + expect.objectContaining({ text: 'code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: 'sinks()', displayText: 'sinks()', type: 'function'}, - {text: 'roots()', displayText: 'roots()', type: 'function'}, - {text: '+', displayText: '+', type: 'up-traversal'}, - {text: '()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: '+'}), + expect.objectContaining({text: '()'}), ], from: 4, // cursor location to: 4, // cursor location @@ -553,53 +405,29 @@ describe('createAssetSelectionHint', () => { it('should handle incomplete and expressions', () => { expect(testAutocomplete('key:"asset1" and |')).toEqual({ list: [ - { + expect.objectContaining({ text: 'key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: 'tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'group:', - displayText: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - attributeName: 'kind', - displayText: 'kind:', - nameBase: false, + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - }, - { + }), + expect.objectContaining({ text: 'code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: 'sinks()', displayText: 'sinks()', type: 'function'}, - {text: 'roots()', displayText: 'roots()', type: 'function'}, - {text: 'not ', displayText: 'not', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'up-traversal'}, - {text: '()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: 'not '}), + expect.objectContaining({text: '+'}), + expect.objectContaining({text: '()'}), ], from: 17, // cursor location to: 17, // cursor location @@ -609,53 +437,29 @@ describe('createAssetSelectionHint', () => { it('should handle incomplete or expressions', () => { expect(testAutocomplete('key:"asset1" or |')).toEqual({ list: [ - { + expect.objectContaining({ text: 'key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: 'tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'group:', - displayText: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - attributeName: 'kind', - displayText: 'kind:', - nameBase: false, + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - }, - { + }), + expect.objectContaining({ text: 'code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: 'sinks()', displayText: 'sinks()', type: 'function'}, - {text: 'roots()', displayText: 'roots()', type: 'function'}, - {text: 'not ', displayText: 'not', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'up-traversal'}, - {text: '()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: 'not '}), + expect.objectContaining({text: '+'}), + expect.objectContaining({text: '()'}), ], from: 16, // cursor location to: 16, // cursor location @@ -665,24 +469,15 @@ describe('createAssetSelectionHint', () => { it('should handle incomplete quoted strings gracefully', () => { expect(testAutocomplete('key:"asse|')).toEqual({ list: [ - { - displayText: 'asset1', + expect.objectContaining({ text: '"asset1"', - type: 'attribute-value', - attributeName: 'key', - }, - { - displayText: 'asset2', + }), + expect.objectContaining({ text: '"asset2"', - type: 'attribute-value', - attributeName: 'key', - }, - { - displayText: 'asset3', + }), + expect.objectContaining({ text: '"asset3"', - type: 'attribute-value', - attributeName: 'key', - }, + }), ], from: 4, // start of value to: 9, // end of value @@ -694,53 +489,29 @@ describe('createAssetSelectionHint', () => { testAutocomplete('sinks(key_substring:"FIVETRAN/google_ads/ad_group_history" or |)'), ).toEqual({ list: [ - { + expect.objectContaining({ text: 'key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: 'tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'group:', - displayText: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - attributeName: 'kind', - displayText: 'kind:', - nameBase: false, + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - }, - { + }), + expect.objectContaining({ text: 'code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: 'sinks()', displayText: 'sinks()', type: 'function'}, - {text: 'roots()', displayText: 'roots()', type: 'function'}, - {text: 'not ', displayText: 'not', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'up-traversal'}, - {text: '()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: 'not '}), + expect.objectContaining({text: '+'}), + expect.objectContaining({text: '()'}), ], from: 62, // cursor location to: 62, // cursor location @@ -752,54 +523,29 @@ describe('createAssetSelectionHint', () => { testAutocomplete('sinks(key_substring:"FIVETRAN/google_ads/ad_group_history" or|)'), ).toEqual({ list: [ - // Inserts a space before the string - { + expect.objectContaining({ text: ' key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: ' tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: ' owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - attributeName: 'group', - displayText: 'group:', - nameBase: false, + }), + expect.objectContaining({ text: ' group:', - type: 'attribute', - }, - { - attributeName: 'kind', - displayText: 'kind:', - nameBase: false, + }), + expect.objectContaining({ text: ' kind:', - type: 'attribute', - }, - { + }), + expect.objectContaining({ text: ' code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: ' sinks()', displayText: 'sinks()', type: 'function'}, - {text: ' roots()', displayText: 'roots()', type: 'function'}, - {text: ' not ', displayText: 'not', type: 'logical_operator'}, - {text: ' +', displayText: '+', type: 'up-traversal'}, - {text: ' ()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: ' sinks()'}), + expect.objectContaining({text: ' roots()'}), + expect.objectContaining({text: ' not '}), + expect.objectContaining({text: ' +'}), + expect.objectContaining({text: ' ()'}), ], from: 61, // cursor location to: 61, // cursor location @@ -813,24 +559,15 @@ describe('createAssetSelectionHint', () => { ), ).toEqual({ list: [ - { + expect.objectContaining({ text: '"asset1"', - displayText: 'asset1', - type: 'attribute-value', - attributeName: 'key_substring', - }, - { + }), + expect.objectContaining({ text: '"asset2"', - displayText: 'asset2', - type: 'attribute-value', - attributeName: 'key_substring', - }, - { + }), + expect.objectContaining({ text: '"asset3"', - displayText: 'asset3', - type: 'attribute-value', - attributeName: 'key_substring', - }, + }), ], from: 76, // cursor location to: 76, // cursor location @@ -839,10 +576,7 @@ describe('createAssetSelectionHint', () => { it('suggestions after downtraversal "+"', () => { expect(testAutocomplete('key:"value"+|')).toEqual({ - list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - ], + list: [expect.objectContaining({text: ' and '}), expect.objectContaining({text: ' or '})], from: 12, // cursor location to: 12, // cursor location }); @@ -850,9 +584,9 @@ describe('createAssetSelectionHint', () => { // UpAndDownTraversal expect(testAutocomplete('+key:"value"|+')).toEqual({ list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'down-traversal'}, + expect.objectContaining({text: ' and '}), + expect.objectContaining({text: ' or '}), + expect.objectContaining({text: '+'}), ], from: 12, // cursor location to: 12, // cursor location @@ -861,9 +595,9 @@ describe('createAssetSelectionHint', () => { // DownTraversal expect(testAutocomplete('key:"value"|+')).toEqual({ list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'down-traversal'}, + expect.objectContaining({text: ' and '}), + expect.objectContaining({text: ' or '}), + expect.objectContaining({text: '+'}), ], from: 11, // cursor location to: 11, // cursor location @@ -875,53 +609,29 @@ describe('createAssetSelectionHint', () => { testAutocomplete('key:"test" or key_substring:"FIVETRAN/google_ads/ad_group_history"+ or |'), ).toEqual({ list: [ - { + expect.objectContaining({ text: 'key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: 'tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'group:', - displayText: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'kind:', - displayText: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: 'sinks()', displayText: 'sinks()', type: 'function'}, - {text: 'roots()', displayText: 'roots()', type: 'function'}, - {text: 'not ', displayText: 'not', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'up-traversal'}, - {text: '()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: 'not '}), + expect.objectContaining({text: '+'}), + expect.objectContaining({text: '()'}), ], from: 71, // cursor position to: 71, // cursor position @@ -931,53 +641,29 @@ describe('createAssetSelectionHint', () => { it('suggestions inside parenthesized expression', () => { expect(testAutocomplete('(|)')).toEqual({ list: [ - { + expect.objectContaining({ text: 'key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: 'tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'group:', - displayText: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'kind:', - displayText: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: 'sinks()', displayText: 'sinks()', type: 'function'}, - {text: 'roots()', displayText: 'roots()', type: 'function'}, - {text: 'not ', displayText: 'not', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'up-traversal'}, - {text: '()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: 'not '}), + expect.objectContaining({text: '+'}), + expect.objectContaining({text: '()'}), ], from: 1, // cursor location to: 1, // cursor location @@ -987,53 +673,29 @@ describe('createAssetSelectionHint', () => { it('suggestions outside parenthesized expression before', () => { expect(testAutocomplete('|()')).toEqual({ list: [ - { + expect.objectContaining({ text: 'key:', - displayText: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { + }), + expect.objectContaining({ text: 'tag:', - displayText: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'owner:', - displayText: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'group:', - displayText: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'kind:', - displayText: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { + }), + expect.objectContaining({ text: 'code_location:', - displayText: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {text: 'sinks()', displayText: 'sinks()', type: 'function'}, - {text: 'roots()', displayText: 'roots()', type: 'function'}, - {text: 'not ', displayText: 'not', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'up-traversal'}, - {text: '()', displayText: '(', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({text: 'not '}), + expect.objectContaining({text: '+'}), + expect.objectContaining({text: '()'}), ], from: 0, // cursor position to: 0, // cursor position @@ -1043,9 +705,9 @@ describe('createAssetSelectionHint', () => { it('suggestions outside parenthesized expression after', () => { expect(testAutocomplete('()|')).toEqual({ list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'down-traversal'}, + expect.objectContaining({text: ' and '}), + expect.objectContaining({text: ' or '}), + expect.objectContaining({text: '+'}), ], from: 2, // cursor position to: 2, // cursor position @@ -1055,9 +717,13 @@ describe('createAssetSelectionHint', () => { it('suggestions within parenthesized expression', () => { expect(testAutocomplete('(tag:"dagster/kind/dlt"|)')).toEqual({ list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'down-traversal'}, + expect.objectContaining({ + text: ' and ', + }), + expect.objectContaining({ + text: ' or ', + }), + expect.objectContaining({text: '+'}), ], from: 23, // cursor position to: 23, // cursor position @@ -1065,9 +731,13 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('sinks(key_substring:"set" or key_substring:"asset"|)')).toEqual({ list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'down-traversal'}, + expect.objectContaining({ + text: ' and ', + }), + expect.objectContaining({ + text: ' or ', + }), + expect.objectContaining({text: '+'}), ], from: 50, // cursor position to: 50, // cursor position @@ -1075,12 +745,9 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('sinks(key_substring:"asset" or key_substring:"s|et2")')).toEqual({ list: [ - { + expect.objectContaining({ text: '"asset2"', - displayText: 'asset2', - type: 'attribute-value', - attributeName: 'key_substring', - }, + }), ], from: 45, // start of value to: 51, // end of value @@ -1088,9 +755,13 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('sinks(key_substring:"sset1"| or key_substring:"set")')).toEqual({ list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - {text: '+', displayText: '+', type: 'down-traversal'}, + expect.objectContaining({ + text: ' and ', + }), + expect.objectContaining({ + text: ' or ', + }), + expect.objectContaining({text: '+'}), ], from: 27, // cursor position to: 27, // cursor position @@ -1099,31 +770,33 @@ describe('createAssetSelectionHint', () => { it('makes suggestions around traversals', () => { expect(testAutocomplete('sinks()+2|')).toEqual({ - list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - ], + list: [expect.objectContaining({text: ' and '}), expect.objectContaining({text: ' or '})], from: 9, // start of value to: 9, // end of value }); expect(testAutocomplete('sinks()+|+')).toEqual({ - list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - ], + list: [expect.objectContaining({text: ' and '}), expect.objectContaining({text: ' or '})], from: 8, // start of value to: 8, // end of value }); expect(testAutocomplete('|2+sinks()+2')).toEqual({ - list: [{text: '()', displayText: '(', type: 'parenthesis'}], + list: [ + expect.objectContaining({ + text: '()', + }), + ], from: 0, // start of value to: 0, // end of value }); expect(testAutocomplete('2|+sinks()+2')).toEqual({ - list: [{text: '()', displayText: '(', type: 'parenthesis'}], + list: [ + expect.objectContaining({ + text: '()', + }), + ], from: 1, // start of value to: 1, // end of value }); @@ -1132,53 +805,33 @@ describe('createAssetSelectionHint', () => { it('makes suggestions for IncompleteExpression inside of the ParenthesizedExpression', () => { expect(testAutocomplete('(key:tag and |)')).toEqual({ list: [ - { - displayText: 'key:', + expect.objectContaining({ text: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - displayText: 'kind:', + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {displayText: 'sinks()', text: 'sinks()', type: 'function'}, - {displayText: 'roots()', text: 'roots()', type: 'function'}, - {displayText: 'not', text: 'not ', type: 'logical_operator'}, - {displayText: '+', text: '+', type: 'up-traversal'}, - {displayText: '(', text: '()', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({ + text: 'not ', + }), + expect.objectContaining({text: '+'}), + expect.objectContaining({ + text: '()', + }), ], from: 13, to: 13, @@ -1186,53 +839,33 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('(key:tag and|)')).toEqual({ list: [ - { - displayText: 'key:', + expect.objectContaining({ text: ' key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: ' tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: ' owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: ' group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - displayText: 'kind:', + }), + expect.objectContaining({ text: ' kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: ' code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {displayText: 'sinks()', text: ' sinks()', type: 'function'}, - {displayText: 'roots()', text: ' roots()', type: 'function'}, - {displayText: 'not', text: ' not ', type: 'logical_operator'}, - {displayText: '+', text: ' +', type: 'up-traversal'}, - {displayText: '(', text: ' ()', type: 'parenthesis'}, + }), + expect.objectContaining({text: ' sinks()'}), + expect.objectContaining({text: ' roots()'}), + expect.objectContaining({ + text: ' not ', + }), + expect.objectContaining({text: ' +'}), + expect.objectContaining({ + text: ' ()', + }), ], from: 12, to: 12, @@ -1240,53 +873,33 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('(key:tag and| )')).toEqual({ list: [ - { - displayText: 'key:', + expect.objectContaining({ text: ' key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: ' tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: ' owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: ' group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - displayText: 'kind:', + }), + expect.objectContaining({ text: ' kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: ' code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {displayText: 'sinks()', text: ' sinks()', type: 'function'}, - {displayText: 'roots()', text: ' roots()', type: 'function'}, - {displayText: 'not', text: ' not ', type: 'logical_operator'}, - {displayText: '+', text: ' +', type: 'up-traversal'}, - {displayText: '(', text: ' ()', type: 'parenthesis'}, + }), + expect.objectContaining({text: ' sinks()'}), + expect.objectContaining({text: ' roots()'}), + expect.objectContaining({ + text: ' not ', + }), + expect.objectContaining({text: ' +'}), + expect.objectContaining({ + text: ' ()', + }), ], from: 12, to: 12, @@ -1295,16 +908,22 @@ describe('createAssetSelectionHint', () => { it('suggestions within incomplete function call expression', () => { expect( - testAutocomplete('(sinks(key:"value"+2 or (key_substring:"aws_cost_report"+2)|'), + testAutocomplete( + '(sinks(key_substring:"FIVETRAN/google_ads/ad_group_history" or (key_substring:"aws_cost_report"+2)|', + ), ).toEqual({ - from: 59, + from: 98, list: [ - {displayText: 'and', text: ' and ', type: 'logical_operator'}, - {displayText: 'or', text: ' or ', type: 'logical_operator'}, - {displayText: '+', text: '+', type: 'down-traversal'}, - {displayText: ')', text: ')', type: 'parenthesis'}, + expect.objectContaining({ + text: ' and ', + }), + expect.objectContaining({ + text: ' or ', + }), + expect.objectContaining({text: '+'}), + expect.objectContaining({text: ')'}), ], - to: 59, + to: 98, }); }); @@ -1314,53 +933,33 @@ describe('createAssetSelectionHint', () => { ).toEqual({ from: 35, list: [ - { - displayText: 'key:', + expect.objectContaining({ text: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - displayText: 'kind:', + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - {displayText: 'sinks()', text: 'sinks()', type: 'function'}, - {displayText: 'roots()', text: 'roots()', type: 'function'}, - {displayText: 'not', text: 'not ', type: 'logical_operator'}, - {displayText: '+', text: '+', type: 'up-traversal'}, - {displayText: '(', text: '()', type: 'parenthesis'}, + }), + expect.objectContaining({text: 'sinks()'}), + expect.objectContaining({text: 'roots()'}), + expect.objectContaining({ + text: 'not ', + }), + expect.objectContaining({text: '+'}), + expect.objectContaining({ + text: '()', + }), ], to: 35, }); @@ -1372,16 +971,12 @@ describe('createAssetSelectionHint', () => { ).toEqual({ from: 32, list: [ - { - displayText: 'or', + expect.objectContaining({ text: 'or', - type: 'logical_operator', - }, - { - displayText: 'and', + }), + expect.objectContaining({ text: 'and', - type: 'logical_operator', - }, + }), ], to: 34, }); @@ -1391,16 +986,12 @@ describe('createAssetSelectionHint', () => { ).toEqual({ from: 32, list: [ - { - displayText: 'and', + expect.objectContaining({ text: 'and', - type: 'logical_operator', - }, - { - displayText: 'or', + }), + expect.objectContaining({ text: 'or', - type: 'logical_operator', - }, + }), ], to: 35, }); @@ -1410,13 +1001,9 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('tag:"dagster/kind/fivetran" or t|:"a"')).toEqual({ from: 31, list: [ - { - displayText: 'tag:', + expect.objectContaining({ text: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, + }), ], to: 33, }); @@ -1426,9 +1013,9 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('tag:"tag|"')).toEqual({ from: 4, list: [ - {text: '"tag1"', displayText: 'tag1', type: 'attribute-value', attributeName: 'tag'}, - {text: '"tag2"', displayText: 'tag2', type: 'attribute-value', attributeName: 'tag'}, - {text: '"tag3"', displayText: 'tag3', type: 'attribute-value', attributeName: 'tag'}, + expect.objectContaining({text: '"tag1"'}), + expect.objectContaining({text: '"tag2"'}), + expect.objectContaining({text: '"tag3"'}), ], to: 9, }); @@ -1442,13 +1029,9 @@ describe('createAssetSelectionHint', () => { ).toEqual({ from: 54, list: [ - { - displayText: 'key:', + expect.objectContaining({ text: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, + }), ], to: 58, }); @@ -1458,9 +1041,9 @@ describe('createAssetSelectionHint', () => { ).toEqual({ from: 58, list: [ - {text: '"asset1"', displayText: 'asset1', type: 'attribute-value', attributeName: 'key'}, - {text: '"asset2"', displayText: 'asset2', type: 'attribute-value', attributeName: 'key'}, - {text: '"asset3"', displayText: 'asset3', type: 'attribute-value', attributeName: 'key'}, + expect.objectContaining({text: '"asset1"'}), + expect.objectContaining({text: '"asset2"'}), + expect.objectContaining({text: '"asset3"'}), ], to: 60, }); @@ -1470,73 +1053,39 @@ describe('createAssetSelectionHint', () => { expect(testAutocomplete('key:"value"+ or tag:"value"+ and owner:"owner" and |')).toEqual({ from: 51, list: [ - { - displayText: 'key:', + expect.objectContaining({ text: 'key:', - type: 'attribute', - attributeName: 'key', - nameBase: true, - }, - { - displayText: 'tag:', + }), + expect.objectContaining({ text: 'tag:', - type: 'attribute', - attributeName: 'tag', - nameBase: false, - }, - { - displayText: 'owner:', + }), + expect.objectContaining({ text: 'owner:', - type: 'attribute', - attributeName: 'owner', - nameBase: false, - }, - { - displayText: 'group:', + }), + expect.objectContaining({ text: 'group:', - type: 'attribute', - attributeName: 'group', - nameBase: false, - }, - { - displayText: 'kind:', + }), + expect.objectContaining({ text: 'kind:', - type: 'attribute', - attributeName: 'kind', - nameBase: false, - }, - { - displayText: 'code_location:', + }), + expect.objectContaining({ text: 'code_location:', - type: 'attribute', - attributeName: 'code_location', - nameBase: false, - }, - { - displayText: 'sinks()', + }), + expect.objectContaining({ text: 'sinks()', - type: 'function', - }, - { - displayText: 'roots()', + }), + expect.objectContaining({ text: 'roots()', - type: 'function', - }, - { - displayText: 'not', + }), + expect.objectContaining({ text: 'not ', - type: 'logical_operator', - }, - { - displayText: '+', + }), + expect.objectContaining({ text: '+', - type: 'up-traversal', - }, - { - displayText: '(', + }), + expect.objectContaining({ text: '()', - type: 'parenthesis', - }, + }), ], to: 51, }); @@ -1545,10 +1094,7 @@ describe('createAssetSelectionHint', () => { it('does not suggest + after +', () => { expect(testAutocomplete('key:"value"+|')).toEqual({ from: 12, - list: [ - {text: ' and ', displayText: 'and', type: 'logical_operator'}, - {text: ' or ', displayText: 'or', type: 'logical_operator'}, - ], + list: [expect.objectContaining({text: ' and '}), expect.objectContaining({text: ' or '})], to: 12, }); }); diff --git a/js_modules/dagster-ui/packages/ui-core/src/selection/useSelectionInputAutoComplete.tsx b/js_modules/dagster-ui/packages/ui-core/src/selection/useSelectionInputAutoComplete.tsx deleted file mode 100644 index dd637adfcf417..0000000000000 --- a/js_modules/dagster-ui/packages/ui-core/src/selection/useSelectionInputAutoComplete.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import {useMemo} from 'react'; - -import {generateAutocompleteResults} from './SelectionAutoComplete'; - -export function useSelectionInputAutoComplete< - T extends Record, - N extends keyof T, ->({ - value, - cursor, - nameBase, - attributesMap, - functions, -}: { - value: string; - cursor: number; - nameBase: N; - attributesMap: T; - functions: string[]; -}) { - const hintFn = useMemo(() => { - return generateAutocompleteResults({nameBase, attributesMap, functions}); - }, [nameBase, attributesMap, functions]); - - const autocompleteResults = useMemo(() => { - return hintFn(value, cursor); - }, [hintFn, value, cursor]); - - return autocompleteResults; -}