diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/Telemetry.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/Telemetry.tsx index ce8133c042eb1..291afd68962fc 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/Telemetry.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/Telemetry.tsx @@ -9,6 +9,7 @@ import {gql} from '../apollo-client'; export enum TelemetryAction { LAUNCH_RUN = 'LAUNCH_RUN', + LAUNCH_MULTIPLE_RUNS = 'LAUNCH_MULTIPLE_RUNS', GRAPHQL_QUERY_COMPLETED = 'GRAPHQL_QUERY_COMPLETED', } @@ -38,7 +39,7 @@ const LOG_TELEMETRY_MUTATION = gql` export async function logTelemetry( pathPrefix: string, action: TelemetryAction, - metadata: {[key: string]: string | null | undefined} = {}, + metadata: {[key: string]: string | string[] | null | undefined} = {}, ) { const graphqlPath = `${pathPrefix || ''}/graphql`; @@ -63,7 +64,10 @@ export async function logTelemetry( export const useTelemetryAction = () => { const {basePath, telemetryEnabled} = useContext(AppContext); return useCallback( - (action: TelemetryAction, metadata: {[key: string]: string | null | undefined} = {}) => { + ( + action: TelemetryAction, + metadata: {[key: string]: string | string[] | null | undefined} = {}, + ) => { if (telemetryEnabled) { logTelemetry(basePath, action, metadata); } diff --git a/js_modules/dagster-ui/packages/ui-core/src/graphql/schema.graphql b/js_modules/dagster-ui/packages/ui-core/src/graphql/schema.graphql index d1e4a9358235c..2a3510755f6dc 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/graphql/schema.graphql +++ b/js_modules/dagster-ui/packages/ui-core/src/graphql/schema.graphql @@ -1511,6 +1511,10 @@ type LaunchRunMutation { Output: LaunchRunResult! } +type LaunchMultipleRunsMutation { + Output: LaunchMultipleRunsResult! +} + type LaunchRunReexecutionMutation { Output: LaunchRunReexecutionResult! } @@ -2982,6 +2986,21 @@ union LaunchRunResult = | ConflictingExecutionParamsError | NoModeProvidedError +union LaunchMultipleRunsResult = + | LaunchMultipleRunsSuccess + | InvalidStepError + | InvalidOutputError + | RunConfigValidationInvalid + | PipelineNotFoundError + | RunConflict + | UnauthorizedError + | PythonError + | InvalidSubsetError + | PresetNotFoundError + | ConflictingExecutionParamsError + | NoModeProvidedError +} + union LaunchRunReexecutionResult = | LaunchRunSuccess | InvalidStepError @@ -3004,6 +3023,10 @@ type LaunchRunSuccess implements LaunchPipelineRunSuccess { run: Run! } +type LaunchMultipleRunsSuccess { + runs: [LaunchRunSuccess!]! +} + union RunsOrError = Runs | InvalidPipelineRunsFilterError | PythonError type Runs implements PipelineRuns { @@ -3693,6 +3716,7 @@ type AutomationConditionEvaluationNode { type Mutation { launchPipelineExecution(executionParams: ExecutionParams!): LaunchRunResult! launchRun(executionParams: ExecutionParams!): LaunchRunResult! + launchMultipleRuns(executionParamsList: [ExecutionParams!]!): LaunchMultipleRunsResult! launchPipelineReexecution( executionParams: ExecutionParams reexecutionParams: ReexecutionParams diff --git a/js_modules/dagster-ui/packages/ui-core/src/graphql/types.ts b/js_modules/dagster-ui/packages/ui-core/src/graphql/types.ts index 6f523f80c5fa0..b5a1da6f5f3f9 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/graphql/types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/graphql/types.ts @@ -2211,11 +2211,20 @@ export type LaunchPipelineRunSuccess = { run: Run; }; +export type LaunchMultipleRunsSuccess = { + runs: Array; +}; + export type LaunchRunMutation = { __typename: 'LaunchRunMutation'; Output: LaunchRunResult; }; +export type LaunchMultipleRunsMutation = { + __typename: 'LaunchMultipleRunsMutation'; + Output: LaunchMultipleRunsResult; +}; + export type LaunchRunReexecutionMutation = { __typename: 'LaunchRunReexecutionMutation'; Output: LaunchRunReexecutionResult; @@ -2249,6 +2258,20 @@ export type LaunchRunResult = | RunConflict | UnauthorizedError; +export type LaunchMultipleRunsResult = + | ConflictingExecutionParamsError + | InvalidOutputError + | InvalidStepError + | InvalidSubsetError + | LaunchMultipleRunsSuccess + | NoModeProvidedError + | PipelineNotFoundError + | PresetNotFoundError + | PythonError + | RunConfigValidationInvalid + | RunConflict + | UnauthorizedError; + export type LaunchRunSuccess = LaunchPipelineRunSuccess & { __typename: 'LaunchRunSuccess'; run: Run; @@ -2619,6 +2642,7 @@ export type Mutation = { launchPipelineExecution: LaunchRunResult; launchPipelineReexecution: LaunchRunReexecutionResult; launchRun: LaunchRunResult; + launchMultipleRuns: LaunchMultipleRunsResult; launchRunReexecution: LaunchRunReexecutionResult; logTelemetry: LogTelemetryMutationResult; reexecutePartitionBackfill: LaunchBackfillResult; @@ -2699,6 +2723,10 @@ export type MutationLaunchRunArgs = { executionParams: ExecutionParams; }; +export type MutationLaunchMultipleRunsArgs = { + executionParamsList: Array; +}; + export type MutationLaunchRunReexecutionArgs = { executionParams?: InputMaybe; reexecutionParams?: InputMaybe; @@ -9453,6 +9481,23 @@ export const buildLaunchRunMutation = ( }; }; +export const buildLaunchMultipleRunsMutation = ( + overrides?: Partial, + _relationshipsToOmit: Set = new Set(), +): {__typename: 'LaunchMultipleRunsMutation'} & LaunchMultipleRunsMutation => { + const relationshipsToOmit: Set = new Set(_relationshipsToOmit); + relationshipsToOmit.add('LaunchMultipleRunsMutation'); + return { + __typename: 'LaunchMultipleRunsMutation', + Output: + overrides && overrides.hasOwnProperty('Output') + ? overrides.Output! + : relationshipsToOmit.has('ConflictingExecutionParamsError') + ? ({} as ConflictingExecutionParamsError) + : buildConflictingExecutionParamsError({}, relationshipsToOmit), + }; +}; + export const buildLaunchRunReexecutionMutation = ( overrides?: Partial, _relationshipsToOmit: Set = new Set(), @@ -10224,6 +10269,12 @@ export const buildMutation = ( : relationshipsToOmit.has('ConflictingExecutionParamsError') ? ({} as ConflictingExecutionParamsError) : buildConflictingExecutionParamsError({}, relationshipsToOmit), + launchMultipleRuns: + overrides && overrides.hasOwnProperty('launchMultipleRuns') + ? overrides.launchMultipleRuns! + : relationshipsToOmit.has('ConflictingExecutionParamsError') + ? ({} as ConflictingExecutionParamsError) + : buildConflictingExecutionParamsError({}, relationshipsToOmit), launchRunReexecution: overrides && overrides.hasOwnProperty('launchRunReexecution') ? overrides.launchRunReexecution! diff --git a/js_modules/dagster-ui/packages/ui-core/src/launchpad/useLaunchMultipleRunsWithTelemetry.oss.tsx b/js_modules/dagster-ui/packages/ui-core/src/launchpad/useLaunchMultipleRunsWithTelemetry.oss.tsx new file mode 100644 index 0000000000000..05a7228e1a771 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/launchpad/useLaunchMultipleRunsWithTelemetry.oss.tsx @@ -0,0 +1,62 @@ +import {useCallback} from 'react'; +import {useHistory} from 'react-router-dom'; + +import {useMutation} from '../apollo-client'; +import {TelemetryAction, useTelemetryAction} from '../app/Telemetry'; +import {showLaunchError} from '../launchpad/showLaunchError'; +import { + LAUNCH_MULTIPLE_RUNS_MUTATION, + LaunchBehavior, + handleLaunchMultipleResult, +} from '../runs/RunUtils'; +import { + LaunchMultipleRunsMutation, + LaunchMultipleRunsMutationVariables, +} from '../runs/types/RunUtils.types'; + +export function useLaunchMultipleRunsWithTelemetry() { + const [launchMultipleRuns] = useMutation< + LaunchMultipleRunsMutation, + LaunchMultipleRunsMutationVariables + >(LAUNCH_MULTIPLE_RUNS_MUTATION); + + const logTelemetry = useTelemetryAction(); + const history = useHistory(); + + return useCallback( + async (variables: LaunchMultipleRunsMutationVariables, behavior: LaunchBehavior) => { + const jobNames = variables.executionParamsList.map((params) => params.selector?.jobName); + + if ( + jobNames.length !== variables.executionParamsList.length || + jobNames.includes(undefined) + ) { + return; + } + + const metadata: {[key: string]: string | string[] | null | undefined} = { + jobNames: jobNames.filter((name): name is string => name !== undefined), + opSelection: undefined, + }; + + let result; + try { + result = (await launchMultipleRuns({variables})).data?.launchMultipleRuns; + if (result) { + handleLaunchMultipleResult(result, history, {behavior}); + logTelemetry( + TelemetryAction.LAUNCH_MULTIPLE_RUNS, + metadata as {[key: string]: string | string[] | null | undefined}, + ); + } + + return result; + } catch (error) { + console.error('error', error); + showLaunchError(error as Error); + } + return undefined; + }, + [history, launchMultipleRuns, logTelemetry], + ); +} diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/RunUtils.tsx b/js_modules/dagster-ui/packages/ui-core/src/runs/RunUtils.tsx index 0c8115d91aa30..4c13f413d45cc 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/RunUtils.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/RunUtils.tsx @@ -7,7 +7,11 @@ import {StepSelection} from './StepSelection'; import {TimeElapsed} from './TimeElapsed'; import {RunFragment} from './types/RunFragments.types'; import {RunTableRunFragment} from './types/RunTableRunFragment.types'; -import {LaunchPipelineExecutionMutation, RunTimeFragment} from './types/RunUtils.types'; +import { + LaunchMultipleRunsMutation, + LaunchPipelineExecutionMutation, + RunTimeFragment, +} from './types/RunUtils.types'; import {Mono} from '../../../ui-components/src'; import {gql} from '../apollo-client'; import {showCustomAlert} from '../app/CustomAlertProvider'; @@ -115,6 +119,73 @@ export async function handleLaunchResult( } } +export async function handleLaunchMultipleResult( + result: void | null | LaunchMultipleRunsMutation['launchMultipleRuns'], + history: History, + options: {behavior: LaunchBehavior; preserveQuerystring?: boolean}, +) { + if (!result) { + showCustomAlert({body: `No data was returned. Did dagster-webserver crash?`}); + return; + } + const successfulRunIds: string[] = []; + + // show corresponding toasts + if (result.__typename === 'LaunchMultipleRunsSuccess') { + for (const individualResult of result.runs) { + successfulRunIds.push(individualResult.run.id); + + const pathname = `/runs/${individualResult.run.id}`; + const search = options.preserveQuerystring ? history.location.search : ''; + const openInSameTab = () => history.push({pathname, search}); + + // using open with multiple runs will spam new tabs + if (options.behavior === 'open') { + openInSameTab(); + } else { + // toast is more preferred + await showSharedToaster({ + intent: 'success', + message: ( +
+ Launched run {individualResult.run.id.slice(0, 8)} +
+ ), + action: { + text: 'View', + href: history.createHref({pathname, search}), + }, + }); + } + document.dispatchEvent(new CustomEvent('run-launched')); + } + } else if (result.__typename === 'InvalidSubsetError') { + showCustomAlert({body: result.message}); + } else if (result.__typename === 'PythonError') { + showCustomAlert({ + title: 'Error', + body: , + }); + } else { + let message = `This multiple job launch cannot be executed with the provided config.`; + + if ('errors' in result) { + message += ` Please fix the following errors:\n\n${result.errors + .map((error) => error.message) + .join('\n\n')}`; + } + + showCustomAlert({body: message}); + } + + // link to runs page filtered to run IDs + const params = new URLSearchParams(); + successfulRunIds.forEach((id) => params.append('q[]', `id:${id}`)); + + const queryString = `/runs?${params.toString()}`; + history.push(queryString); +} + function getBaseExecutionMetadata(run: RunFragment | RunTableRunFragment) { const hiddenTagKeys: string[] = [DagsterTag.IsResumeRetry, DagsterTag.StepSelection]; @@ -204,6 +275,37 @@ export const LAUNCH_PIPELINE_EXECUTION_MUTATION = gql` ${PYTHON_ERROR_FRAGMENT} `; +export const LAUNCH_MULTIPLE_RUNS_MUTATION = gql` + mutation LaunchMultipleRuns($executionParamsList: [ExecutionParams!]!) { + launchMultipleRuns(executionParamsList: $executionParamsList) { + ... on LaunchMultipleRunsSuccess { + runs { + ... on LaunchRunSuccess { + run { + id + jobName + } + } + } + } + ... on PipelineNotFoundError { + message + } + ... on InvalidSubsetError { + message + } + ... on RunConfigValidationInvalid { + errors { + message + } + } + ...PythonErrorFragment + } + } + + ${PYTHON_ERROR_FRAGMENT} +`; + export const DELETE_MUTATION = gql` mutation Delete($runId: String!) { deletePipelineRun(runId: $runId) { diff --git a/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunUtils.types.ts b/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunUtils.types.ts index f9dbe944212ae..46feb4e0249e1 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunUtils.types.ts +++ b/js_modules/dagster-ui/packages/ui-core/src/runs/types/RunUtils.types.ts @@ -42,6 +42,55 @@ export type LaunchPipelineExecutionMutation = { | {__typename: 'UnauthorizedError'}; }; +export type LaunchMultipleRunsMutationVariables = Types.Exact<{ + executionParamsList: Array; +}>; + +export type LaunchMultipleRunsMutation = { + __typename: 'Mutation'; + launchMultipleRuns: + | {__typename: 'ConflictingExecutionParamsError'} + | {__typename: 'InvalidOutputError'} + | {__typename: 'InvalidStepError'} + | {__typename: 'InvalidSubsetError'; message: string} + | { + __typename: 'LaunchMultipleRunsSuccess'; + runs: Array<{ + __typename: 'LaunchRunSuccess'; + run: { + id: string; + jobName: string; + }; + }>; + } + | {__typename: 'NoModeProvidedError'} + | {__typename: 'PipelineNotFoundError'; message: string} + | {__typename: 'PresetNotFoundError'} + | { + __typename: 'PythonError'; + message: string; + stack: Array; + errorChain: Array<{ + __typename: 'ErrorChainLink'; + isExplicitLink: boolean; + error: {__typename: 'PythonError'; message: string; stack: Array}; + }>; + } + | { + __typename: 'RunConfigValidationInvalid'; + errors: Array< + | {__typename: 'FieldNotDefinedConfigError'; message: string} + | {__typename: 'FieldsNotDefinedConfigError'; message: string} + | {__typename: 'MissingFieldConfigError'; message: string} + | {__typename: 'MissingFieldsConfigError'; message: string} + | {__typename: 'RuntimeMismatchConfigError'; message: string} + | {__typename: 'SelectorTypeConfigError'; message: string} + >; + } + | {__typename: 'RunConflict'} + | {__typename: 'UnauthorizedError'}; +}; + export type DeleteMutationVariables = Types.Exact<{ runId: Types.Scalars['String']['input']; }>; @@ -166,10 +215,12 @@ export type RunTimeFragment = { updateTime: number | null; }; -export const LaunchPipelineExecutionVersion = '292088c4a697aca6be1d3bbc0cfc45d8a13cdb2e75cfedc64b68c6245ea34f89'; +export const LaunchPipelineExecutionVersion = + '292088c4a697aca6be1d3bbc0cfc45d8a13cdb2e75cfedc64b68c6245ea34f89'; export const DeleteVersion = '3c61c79b99122910e754a8863e80dc5ed479a0c23cc1a9d9878d91e603fc0dfe'; export const TerminateVersion = '67acf403eb320a93c9a9aa07f675a1557e0887d499cd5598f1d5ff360afc15c0'; -export const LaunchPipelineReexecutionVersion = 'd21e4ecaf3d1d163c4772f1d847dbdcbdaa9a40e6de0808a064ae767adf0c311'; +export const LaunchPipelineReexecutionVersion = + 'd21e4ecaf3d1d163c4772f1d847dbdcbdaa9a40e6de0808a064ae767adf0c311'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/ticks/SensorDryRunDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/ticks/SensorDryRunDialog.tsx index 0af78ed4cea37..190f169ef09aa 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/ticks/SensorDryRunDialog.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/ticks/SensorDryRunDialog.tsx @@ -13,15 +13,18 @@ import { Subheading, Tag, TextInput, + Tooltip, } from '@dagster-io/ui-components'; import {useCallback, useMemo, useState} from 'react'; +import {useLaunchMultipleRunsWithTelemetry} from 'shared/launchpad/useLaunchMultipleRunsWithTelemetry.oss'; import styled from 'styled-components'; import {RunRequestTable} from './DryRunRequestTable'; import {DynamicPartitionRequests} from './DynamicPartitionRequests'; -import {RUN_REQUEST_FRAGMENT} from './RunRequestFragment'; import {gql, useMutation} from '../apollo-client'; +import {RUN_REQUEST_FRAGMENT} from './RunRequestFragment'; import { + SensorDryRunInstigationTick, SensorDryRunMutation, SensorDryRunMutationVariables, } from './types/SensorDryRunDialog.types'; @@ -31,19 +34,16 @@ import {PYTHON_ERROR_FRAGMENT} from '../app/PythonErrorFragment'; import {PythonErrorInfo} from '../app/PythonErrorInfo'; import {assertUnreachable} from '../app/Util'; import {PythonErrorFragment} from '../app/types/PythonErrorFragment.types'; +import {ExecutionParams, SensorSelector} from '../graphql/types'; import {SET_CURSOR_MUTATION} from '../sensors/EditCursorDialog'; import { SetSensorCursorMutation, SetSensorCursorMutationVariables, } from '../sensors/types/EditCursorDialog.types'; import {testId} from '../testing/testId'; +import {buildExecutionParamsListSensor} from '../util/buildExecutionParamsList'; import {RepoAddress} from '../workspace/types'; -type DryRunInstigationTick = Extract< - SensorDryRunMutation['sensorDryRun'], - {__typename: 'DryRunInstigationTick'} ->; - type Props = { name: string; onClose: () => void; @@ -76,12 +76,13 @@ const SensorDryRun = ({repoAddress, name, currentCursor, onClose, jobName}: Prop const [cursor, setCursor] = useState(currentCursor); const [submitting, setSubmitting] = useState(false); + const [launching, setLaunching] = useState(false); const [error, setError] = useState(null); - const [sensorExecutionData, setSensorExecutionData] = useState( - null, - ); + const [sensorExecutionData, setSensorExecutionData] = + useState(null); + const [executionParamsList, setExecutionParamsList] = useState([]); - const sensorSelector = useMemo( + const sensorSelector: SensorSelector = useMemo( () => ({ sensorName: name, repositoryLocationName: repoAddress.location, @@ -105,6 +106,7 @@ const SensorDryRun = ({repoAddress, name, currentCursor, onClose, jobName}: Prop setError(data.evaluationResult.error); } else { setSensorExecutionData(data); + setExecutionParamsList(buildExecutionParamsListSensor(data, sensorSelector)); } } else if (data?.__typename === 'SensorNotFoundError') { showCustomAlert({ @@ -120,16 +122,47 @@ const SensorDryRun = ({repoAddress, name, currentCursor, onClose, jobName}: Prop setSubmitting(false); }, [sensorDryRun, sensorSelector, cursor, name]); + const launchMultipleRunsWithTelemetry = useLaunchMultipleRunsWithTelemetry(); + + const canLaunchAll = useMemo(() => { + return executionParamsList != null && executionParamsList.length > 0; + }, [executionParamsList]); + + const onLaunchAll = async () => { + if (!canLaunchAll) { + return; + } + setLaunching(true); + + await launchMultipleRunsWithTelemetry({executionParamsList}, 'toast'); + + setLaunching(false); + onClose(); + }; + const buttons = useMemo(() => { + if (launching) { + return ; + } + if (sensorExecutionData || error) { return ( - + + +