diff --git a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/AntlrAssetSelection.ts b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/AntlrAssetSelection.ts index d39434b2d0201..9a32b9ba87f68 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/asset-selection/AntlrAssetSelection.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/asset-selection/AntlrAssetSelection.ts @@ -9,6 +9,7 @@ import {FeatureFlag} from 'shared/app/FeatureFlags.oss'; import {AntlrAssetSelectionVisitor} from './AntlrAssetSelectionVisitor'; import {AssetGraphQueryItem} from '../asset-graph/useAssetGraphData'; +import {weakMapMemoize} from '../util/weakMapMemoize'; import {AssetSelectionLexer} from './generated/AssetSelectionLexer'; import {AssetSelectionParser} from './generated/AssetSelectionParser'; import {featureEnabled} from '../app/Flags'; @@ -65,17 +66,17 @@ export const parseAssetSelectionQuery = ( } }; -export const filterAssetSelectionByQuery = ( - all_assets: AssetGraphQueryItem[], - query: string, -): AssetSelectionQueryResult => { - if (featureEnabled(FeatureFlag.flagAssetSelectionSyntax)) { - const result = parseAssetSelectionQuery(all_assets, query); - if (result instanceof Error) { - // fall back to old behavior - return filterByQuery(all_assets, query); +export const filterAssetSelectionByQuery = weakMapMemoize( + (all_assets: AssetGraphQueryItem[], query: string): AssetSelectionQueryResult => { + if (featureEnabled(FeatureFlag.flagAssetSelectionSyntax)) { + const result = parseAssetSelectionQuery(all_assets, query); + if (result instanceof Error) { + // fall back to old behavior + return filterByQuery(all_assets, query); + } + return result; } - return result; - } - return filterByQuery(all_assets, query); -}; + return filterByQuery(all_assets, query); + }, + {maxEntries: 20}, +); diff --git a/js_modules/dagster-ui/packages/ui-core/src/assets/VirtualizedSimpleAssetKeyList.tsx b/js_modules/dagster-ui/packages/ui-core/src/assets/VirtualizedSimpleAssetKeyList.tsx index 7404111fc5014..0e5e7bf441615 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/assets/VirtualizedSimpleAssetKeyList.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/assets/VirtualizedSimpleAssetKeyList.tsx @@ -13,7 +13,7 @@ export const VirtualizedSimpleAssetKeyList = ({ style, }: { assetKeys: AssetKeyInput[]; - style: CSSProperties; + style?: CSSProperties; }) => { const parentRef = useRef(null); const rowVirtualizer = useVirtualizer({ diff --git a/js_modules/dagster-ui/packages/ui-core/src/gantt/GanttChart.tsx b/js_modules/dagster-ui/packages/ui-core/src/gantt/GanttChart.tsx index 39b4fcdc0f4e8..179e15ac0d2ef 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/gantt/GanttChart.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/gantt/GanttChart.tsx @@ -15,6 +15,7 @@ import isEqual from 'lodash/isEqual'; import * as React from 'react'; import {useMemo} from 'react'; import {Link} from 'react-router-dom'; +import {FeatureFlag} from 'shared/app/FeatureFlags.oss'; import styled from 'styled-components'; import { @@ -48,6 +49,7 @@ import { interestingQueriesFor, } from './GanttChartLayout'; import {GanttChartModeControl} from './GanttChartModeControl'; +import {GanttChartSelectionInput} from './GanttChartSelectionInput'; import {GanttChartTimescale} from './GanttChartTimescale'; import {GanttStatusPanel} from './GanttStatusPanel'; import {OptionsContainer, OptionsSpacer} from './VizComponents'; @@ -55,6 +57,7 @@ import {ZoomSlider} from './ZoomSlider'; import {RunGraphQueryItem} from './toGraphQueryItems'; import {useGanttChartMode} from './useGanttChartMode'; import {AppContext} from '../app/AppContext'; +import {featureEnabled} from '../app/Flags'; import {GraphQueryItem} from '../app/GraphQueryImpl'; import {withMiddleTruncation} from '../app/Util'; import {WebSocketContext} from '../app/WebSocketProvider'; @@ -411,14 +414,22 @@ const GanttChartInner = React.memo((props: GanttChartInnerProps) => { ) : null} - 0 ? 'has-step' : ''} - /> + {featureEnabled(FeatureFlag.flagRunSelectionSyntax) ? ( + + ) : ( + 0 ? 'has-step' : ''} + /> + )} void; +}) => { + 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]); + + return ( + + + + ); +}; + +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/run-selection/AntlrRunSelection.ts b/js_modules/dagster-ui/packages/ui-core/src/run-selection/AntlrRunSelection.ts index ae19f75aa464b..d93abfb242675 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/run-selection/AntlrRunSelection.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/run-selection/AntlrRunSelection.ts @@ -8,8 +8,9 @@ import {RunSelectionLexer} from './generated/RunSelectionLexer'; import {RunSelectionParser} from './generated/RunSelectionParser'; import {featureEnabled} from '../app/Flags'; import {filterByQuery} from '../app/GraphQueryImpl'; +import {weakMapMemoize} from '../util/weakMapMemoize'; -type RunSelectionQueryResult = { +export type RunSelectionQueryResult = { all: RunGraphQueryItem[]; focus: RunGraphQueryItem[]; }; @@ -44,17 +45,17 @@ export const parseRunSelectionQuery = ( } }; -export const filterRunSelectionByQuery = ( - all_runs: RunGraphQueryItem[], - query: string, -): RunSelectionQueryResult => { - if (featureEnabled(FeatureFlag.flagRunSelectionSyntax)) { - const result = parseRunSelectionQuery(all_runs, query); - if (result instanceof Error) { - // fall back to old behavior - return filterByQuery(all_runs, query); +export const filterRunSelectionByQuery = weakMapMemoize( + (all_runs: RunGraphQueryItem[], query: string): RunSelectionQueryResult => { + if (featureEnabled(FeatureFlag.flagRunSelectionSyntax)) { + const result = parseRunSelectionQuery(all_runs, query); + if (result instanceof Error) { + // fall back to old behavior + return filterByQuery(all_runs, query); + } + return result; } - return result; - } - return filterByQuery(all_runs, query); -}; + return filterByQuery(all_runs, query); + }, + {maxEntries: 20}, +); diff --git a/js_modules/dagster-ui/packages/ui-core/src/run-selection/AntlrRunSelectionVisitor.ts b/js_modules/dagster-ui/packages/ui-core/src/run-selection/AntlrRunSelectionVisitor.ts index c1f457693062a..97983129733f9 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/run-selection/AntlrRunSelectionVisitor.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/run-selection/AntlrRunSelectionVisitor.ts @@ -160,6 +160,12 @@ export class AntlrRunSelectionVisitor visitStatusAttributeExpr(ctx: StatusAttributeExprContext) { const state: string = getValue(ctx.value()).toLowerCase(); - return new Set([...this.all_runs].filter((i) => i.metadata?.state === state)); + return new Set( + [...this.all_runs].filter( + (i) => i.metadata?.state === state || (state === NO_STATE && !i.metadata?.state), + ), + ); } } + +export const NO_STATE = 'none'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/Run.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/Run.tsx index 0d227c3fa1bde..4fd391633cf5c 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/Run.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/Run.tsx @@ -10,7 +10,8 @@ import { Tooltip, } from '@dagster-io/ui-components'; import * as React from 'react'; -import {memo} from 'react'; +import {memo, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import {FeatureFlag} from 'shared/app/FeatureFlags.oss'; import styled from 'styled-components'; import {CapturedOrExternalLogPanel} from './CapturedLogPanel'; @@ -27,7 +28,7 @@ import { } from './useComputeLogFileKeyForSelection'; import {useQueryPersistedLogFilter} from './useQueryPersistedLogFilter'; import {showCustomAlert} from '../app/CustomAlertProvider'; -import {filterByQuery} from '../app/GraphQueryImpl'; +import {featureEnabled} from '../app/Flags'; import {PythonErrorInfo} from '../app/PythonErrorInfo'; import {isHiddenAssetGroupJob} from '../asset-graph/Utils'; import {GanttChart, GanttChartLoadingState, GanttChartMode, QueuedState} from '../gantt/GanttChart'; @@ -37,6 +38,7 @@ import {useDocumentTitle} from '../hooks/useDocumentTitle'; import {useFavicon} from '../hooks/useFavicon'; import {useQueryPersistedState} from '../hooks/useQueryPersistedState'; import {CompletionType, useTraceDependency} from '../performance/TraceContext'; +import {filterRunSelectionByQuery} from '../run-selection/AntlrRunSelection'; interface RunProps { runId: string; @@ -103,7 +105,7 @@ export const Run = memo((props: RunProps) => { {(logs) => ( <> - + {(metadata) => ( { ); }); -const OnLogsLoaded = ({dependency}: {dependency: ReturnType}) => { - React.useLayoutEffect(() => { - dependency.completeDependency(CompletionType.SUCCESS); - }, [dependency]); +const OnLogsLoaded = ({ + dependency, + logs, +}: { + dependency: ReturnType; + logs: LogsProviderLogs; +}) => { + useLayoutEffect(() => { + if (!logs.loading) { + dependency.completeDependency(CompletionType.SUCCESS); + } + }, [dependency, logs]); return null; }; @@ -179,6 +189,8 @@ const RunWithData = ({ onSetLogsFilter, onSetSelectionQuery, }: RunWithDataProps) => { + const newRunSelectionSyntax = featureEnabled(FeatureFlag.flagRunSelectionSyntax); + const [queryLogType, setQueryLogType] = useQueryPersistedState({ queryKey: 'logType', defaults: {logType: LogType.structured}, @@ -186,19 +198,27 @@ const RunWithData = ({ const logType = logTypeFromQuery(queryLogType); const setLogType = (lt: LogType) => setQueryLogType(LogType[lt]); - const [computeLogUrl, setComputeLogUrl] = React.useState(null); + const [computeLogUrl, setComputeLogUrl] = useState(null); const stepKeysJSON = JSON.stringify(Object.keys(metadata.steps).sort()); - const stepKeys = React.useMemo(() => JSON.parse(stepKeysJSON), [stepKeysJSON]); + const stepKeys = useMemo(() => JSON.parse(stepKeysJSON), [stepKeysJSON]); const runtimeGraph = run?.executionPlan && toGraphQueryItems(run?.executionPlan, metadata.steps); - const selectionStepKeys = React.useMemo(() => { + const selectionStepKeys = useMemo(() => { return runtimeGraph && selectionQuery && selectionQuery !== '*' - ? filterByQuery(runtimeGraph, selectionQuery).all.map((n) => n.name) + ? filterRunSelectionByQuery(runtimeGraph, selectionQuery).all.map((n) => n.name) : []; }, [runtimeGraph, selectionQuery]); + const selection = useMemo( + () => ({ + query: selectionQuery, + keys: selectionStepKeys, + }), + [selectionStepKeys, selectionQuery], + ); + const {logCaptureInfo, computeLogFileKey, setComputeLogFileKey} = useComputeLogFileKeyForSelection({ stepKeys, @@ -207,19 +227,26 @@ const RunWithData = ({ defaultToFirstStep: false, }); - const logsFilterStepKeys = runtimeGraph - ? logsFilter.logQuery - .filter((v) => v.token && v.token === 'query') - .reduce((accum, v) => { - accum.push(...filterByQuery(runtimeGraph, v.value).all.map((n) => n.name)); - return accum; - }, [] as string[]) - : []; + const logsFilterStepKeys = useMemo( + () => + runtimeGraph + ? logsFilter.logQuery + .filter((v) => v.token && v.token === 'query') + .reduce((accum, v) => { + accum.push( + ...filterRunSelectionByQuery(runtimeGraph, v.value).all.map((n) => n.name), + ); + return accum; + }, [] as string[]) + : [], + [logsFilter.logQuery, runtimeGraph], + ); const onClickStep = (stepKey: string, evt: React.MouseEvent) => { const index = selectionStepKeys.indexOf(stepKey); - let newSelected: string[]; + let newSelected: string[] = []; const filterForExactStep = `"${stepKey}"`; + let nextSelectionQuery = selectionQuery; if (evt.shiftKey) { // shift-click to multi select steps, preserving quotations if present newSelected = [ @@ -228,18 +255,34 @@ const RunWithData = ({ if (index !== -1) { // deselect the step if already selected - newSelected.splice(index, 1); + if (newRunSelectionSyntax) { + nextSelectionQuery = removeStepFromSelection(nextSelectionQuery, stepKey); + } else { + newSelected.splice(index, 1); + } } else { // select the step otherwise - newSelected.push(filterForExactStep); + if (newRunSelectionSyntax) { + nextSelectionQuery = addStepToSelection(nextSelectionQuery, stepKey); + } else { + newSelected.push(filterForExactStep); + } } } else { + // deselect the step if already selected if (selectionStepKeys.length === 1 && index !== -1) { - // deselect the step if already selected - newSelected = []; + if (newRunSelectionSyntax) { + nextSelectionQuery = ''; + } else { + newSelected = []; + } } else { // select the step otherwise - newSelected = [filterForExactStep]; + if (newRunSelectionSyntax) { + nextSelectionQuery = `name:"${stepKey}"`; + } else { + newSelected = [filterForExactStep]; + } // When only one step is selected, set the compute log key as well. const matchingLogKey = matchingComputeLogKeyFromStepKey(metadata.logCaptureSteps, stepKey); @@ -249,13 +292,17 @@ const RunWithData = ({ } } - onSetSelectionQuery(newSelected.join(', ') || '*'); + if (newRunSelectionSyntax) { + onSetSelectionQuery(nextSelectionQuery); + } else { + onSetSelectionQuery(newSelected.join(', ') || '*'); + } }; - const [expandedPanel, setExpandedPanel] = React.useState(null); - const containerRef = React.useRef(null); + const [expandedPanel, setExpandedPanel] = useState(null); + const containerRef = useRef(null); - React.useEffect(() => { + useLayoutEffect(() => { if (containerRef.current) { const size = containerRef.current.getSize(); if (size === 100) { @@ -310,14 +357,14 @@ const RunWithData = ({ run={run} graph={runtimeGraph} metadata={metadata} - selection={{query: selectionQuery, keys: selectionStepKeys}} + selection={selection} /> } runId={runId} graph={runtimeGraph} metadata={metadata} - selection={{query: selectionQuery, keys: selectionStepKeys}} + selection={selection} onClickStep={onClickStep} onSetSelection={onSetSelectionQuery} focusedTime={logsFilter.focusedTime} @@ -409,3 +456,11 @@ const NoStepSelectionState = ({type}: {type: LogType}) => { ); }; + +function removeStepFromSelection(selectionQuery: string, stepKey: string) { + return `(${selectionQuery}) and not name:"${stepKey}"`; +} + +function addStepToSelection(selectionQuery: string, stepKey: string) { + return `(${selectionQuery}) or name:"${stepKey}"`; +} diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunActionButtons.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunActionButtons.tsx index 4143f0cc979d3..5d520b7d0bc5a 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/RunActionButtons.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunActionButtons.tsx @@ -1,5 +1,6 @@ import {Box, Button, Group, Icon} from '@dagster-io/ui-components'; import {useCallback, useState} from 'react'; +import {FeatureFlag} from 'shared/app/FeatureFlags.oss'; import {IRunMetadataDict, IStepState} from './RunMetadataProvider'; import {doneStatuses, failedStatuses} from './RunStatuses'; @@ -11,10 +12,12 @@ import {RunFragment, RunPageFragment} from './types/RunFragments.types'; import {useJobAvailabilityErrorForRun} from './useJobAvailabilityErrorForRun'; import {useJobReexecution} from './useJobReExecution'; import {showSharedToaster} from '../app/DomUtils'; +import {featureEnabled} from '../app/Flags'; import {GraphQueryItem, filterByQuery} from '../app/GraphQueryImpl'; import {DEFAULT_DISABLED_REASON} from '../app/Permissions'; import {ReexecutionStrategy} from '../graphql/types'; import {LaunchButtonConfiguration, LaunchButtonDropdown} from '../launchpad/LaunchButton'; +import {filterRunSelectionByQuery} from '../run-selection/AntlrRunSelection'; import {useRepositoryForRunWithParentSnapshot} from '../workspace/useRepositoryForRun'; interface RunActionButtonsProps { @@ -185,8 +188,11 @@ export const RunActionButtons = (props: RunActionButtonsProps) => { console.warn('Run execution plan must be present to launch from-selected execution'); return Promise.resolve(); } - const selectionAndDownstreamQuery = selection.keys.map((k) => `${k}*`).join(','); - const selectionKeys = filterByQuery(graph, selectionAndDownstreamQuery).all.map( + const selectionAndDownstreamQuery = featureEnabled(FeatureFlag.flagRunSelectionSyntax) + ? selection.keys.map((k) => `name:"${k}"*`).join(' or ') + : selection.keys.map((k) => `${k}*`).join(','); + + const selectionKeys = filterRunSelectionByQuery(graph, selectionAndDownstreamQuery).all.map( (node) => node.name, ); diff --git a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteInput.tsx b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteInput.tsx index e84b930cf248d..e021299a40ee4 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteInput.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/selection/SelectionAutoCompleteInput.tsx @@ -179,8 +179,10 @@ export const InputDiv = styled.div` ${SelectionAutoCompleteInputCSS} `; +// Z-index: 21 to beat out Dialog's z-index: 20 const GlobalHintStyles = createGlobalStyle` .CodeMirror-hints { + z-index: 21; background: ${Colors.popoverBackground()}; border: none; border-radius: 4px; @@ -201,12 +203,14 @@ const GlobalHintStyles = createGlobalStyle` function showHint(instance: Editor, hint: HintFunction) { requestAnimationFrame(() => { requestAnimationFrame(() => { - instance.showHint({ - hint, - completeSingle: false, - moveOnOverlap: true, - updateOnCursorActivity: true, - }); + if (instance.getWrapperElement().contains(document.activeElement)) { + instance.showHint({ + hint, + completeSingle: false, + moveOnOverlap: true, + updateOnCursorActivity: true, + }); + } }); }); } diff --git a/js_modules/dagster-ui/packages/ui-core/src/workspace/WorkspaceContext/WorkspaceContext.tsx b/js_modules/dagster-ui/packages/ui-core/src/workspace/WorkspaceContext/WorkspaceContext.tsx index 4870011c261eb..cd60b1e776c55 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/workspace/WorkspaceContext/WorkspaceContext.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/workspace/WorkspaceContext/WorkspaceContext.tsx @@ -62,9 +62,18 @@ interface WorkspaceState { setHidden: SetVisibleOrHiddenFn; } -export const WorkspaceContext = React.createContext( - new Error('WorkspaceContext should never be uninitialized') as any, -); +export const WorkspaceContext = React.createContext({ + allRepos: [], + visibleRepos: [], + data: {}, + refetch: () => Promise.reject(), + toggleVisible: () => {}, + loading: false, + locationEntries: [], + locationStatuses: {}, + setVisible: () => {}, + setHidden: () => {}, +}); interface BaseLocationParams { localCacheIdPrefix?: string;