diff --git a/js_modules/dagster-ui/packages/ui-core/client.json b/js_modules/dagster-ui/packages/ui-core/client.json index 4c62310e025b4..9b8c430dd2ead 100644 --- a/js_modules/dagster-ui/packages/ui-core/client.json +++ b/js_modules/dagster-ui/packages/ui-core/client.json @@ -126,6 +126,7 @@ "RunRootQuery": "1aa4561b33c2cfb079d7a3ff284096fc3208a46dee748a24c7af827a2cb22919", "RunStatsQuery": "75e80f740a79607de9e1152f9b7074d319197fbc219784c767c1abd5553e9a49", "LaunchPipelineExecution": "292088c4a697aca6be1d3bbc0cfc45d8a13cdb2e75cfedc64b68c6245ea34f89", + "LaunchMultipleRuns": "a56d9efdb35e71e0fd1744dd768129248943bc5b23e717458b82c46829661763", "Delete": "3c61c79b99122910e754a8863e80dc5ed479a0c23cc1a9d9878d91e603fc0dfe", "Terminate": "67acf403eb320a93c9a9aa07f675a1557e0887d499cd5598f1d5ff360afc15c0", "LaunchPipelineReexecution": "d21e4ecaf3d1d163c4772f1d847dbdcbdaa9a40e6de0808a064ae767adf0c311", 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/launchpad/useLaunchMultipleRunsWithTelemetry.ts b/js_modules/dagster-ui/packages/ui-core/src/launchpad/useLaunchMultipleRunsWithTelemetry.ts new file mode 100644 index 0000000000000..f79ab12cea078 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/launchpad/useLaunchMultipleRunsWithTelemetry.ts @@ -0,0 +1,62 @@ +import {useCallback} from 'react'; +import {useHistory} from 'react-router-dom'; + +import {showLaunchError} from './showLaunchError'; +import {useMutation} from '../apollo-client'; +import {TelemetryAction, useTelemetryAction} from '../app/Telemetry'; +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 executionParamsList = Array.isArray(variables.executionParamsList) + ? variables.executionParamsList + : [variables.executionParamsList]; + const jobNames = executionParamsList.map((params) => params.selector?.jobName); + + if (jobNames.length !== 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..6a17b5cfc530c 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'; @@ -107,7 +111,7 @@ export async function handleLaunchResult( if ('errors' in result) { message += ` Please fix the following errors:\n\n${result.errors - .map((error) => error.message) + .map((error: {message: any}) => error.message) .join('\n\n')}`; } @@ -115,6 +119,91 @@ 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[] = []; + const failedRunsErrors: {message: string}[] = []; + + if (result.__typename === 'PythonError') { + // if launch multiple runs errors out, show the PythonError and return + showCustomAlert({ + title: 'Error', + body: , + }); + return; + } else if (result.__typename === 'LaunchMultipleRunsResult') { + // show corresponding toasts + const launchMultipleRunsResult = result.launchMultipleRunsResult; + + for (const individualResult of launchMultipleRunsResult) { + if (individualResult.__typename === 'LaunchRunSuccess') { + 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 if (individualResult.__typename === 'PythonError') { + failedRunsErrors.push({message: individualResult.message}); + } else { + let message = `Error launching run.`; + if ( + individualResult && + typeof individualResult === 'object' && + 'errors' in individualResult + ) { + const errors = individualResult.errors as {message: string}[]; + message += ` Please fix the following errors:\n\n${errors + .map((error) => error.message) + .join('\n\n')}`; + } + if ( + individualResult && + typeof individualResult === 'object' && + 'message' in individualResult + ) { + message += `\n\n${individualResult.message}`; + } + + failedRunsErrors.push({message}); + } + } + } + document.dispatchEvent(new CustomEvent('run-launched')); + + // 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); + + await showSharedToaster({ + intent: 'success', + message:
Launched {successfulRunIds.length} runs
, + action: { + text: 'View', + href: history.createHref({pathname: queryString}), + }, + }); + + // show list of errors that occurred + if (failedRunsErrors.length > 0) { + showCustomAlert({body: failedRunsErrors.map((e) => e.message).join('\n\n')}); + } +} + function getBaseExecutionMetadata(run: RunFragment | RunTableRunFragment) { const hiddenTagKeys: string[] = [DagsterTag.IsResumeRetry, DagsterTag.StepSelection]; @@ -204,6 +293,65 @@ export const LAUNCH_PIPELINE_EXECUTION_MUTATION = gql` ${PYTHON_ERROR_FRAGMENT} `; +export const LAUNCH_MULTIPLE_RUNS_MUTATION = gql` + mutation LaunchMultipleRuns($executionParamsList: [ExecutionParams!]!) { + launchMultipleRuns(executionParamsList: $executionParamsList) { + __typename + ... on LaunchMultipleRunsResult { + launchMultipleRunsResult { + __typename + ... on InvalidStepError { + invalidStepKey + } + ... on InvalidOutputError { + stepKey + invalidOutputName + } + ... on LaunchRunSuccess { + run { + id + pipeline { + name + } + tags { + key + value + } + status + runConfigYaml + mode + resolvedOpSelection + } + } + ... on ConflictingExecutionParamsError { + message + } + ... on PresetNotFoundError { + preset + message + } + ... on RunConfigValidationInvalid { + pipelineName + errors { + __typename + message + path + reason + } + } + ... on PipelineNotFoundError { + message + pipelineName + } + ...PythonErrorFragment + } + } + ...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..0ce96156d07a9 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,106 @@ export type LaunchPipelineExecutionMutation = { | {__typename: 'UnauthorizedError'}; }; +export type LaunchMultipleRunsMutationVariables = Types.Exact<{ + executionParamsList: Array | Types.ExecutionParams; +}>; + +export type LaunchMultipleRunsMutation = { + __typename: 'Mutation'; + launchMultipleRuns: + | { + __typename: 'LaunchMultipleRunsResult'; + launchMultipleRunsResult: Array< + | {__typename: 'ConflictingExecutionParamsError'; message: string} + | {__typename: 'InvalidOutputError'; stepKey: string; invalidOutputName: string} + | {__typename: 'InvalidStepError'; invalidStepKey: string} + | {__typename: 'InvalidSubsetError'} + | { + __typename: 'LaunchRunSuccess'; + run: { + __typename: 'Run'; + id: string; + status: Types.RunStatus; + runConfigYaml: string; + mode: string; + resolvedOpSelection: Array | null; + pipeline: + | {__typename: 'PipelineSnapshot'; name: string} + | {__typename: 'UnknownPipeline'; name: string}; + tags: Array<{__typename: 'PipelineTag'; key: string; value: string}>; + }; + } + | {__typename: 'NoModeProvidedError'} + | {__typename: 'PipelineNotFoundError'; message: string; pipelineName: string} + | {__typename: 'PresetNotFoundError'; preset: string; message: string} + | { + __typename: 'PythonError'; + message: string; + stack: Array; + errorChain: Array<{ + __typename: 'ErrorChainLink'; + isExplicitLink: boolean; + error: {__typename: 'PythonError'; message: string; stack: Array}; + }>; + } + | { + __typename: 'RunConfigValidationInvalid'; + pipelineName: string; + errors: Array< + | { + __typename: 'FieldNotDefinedConfigError'; + message: string; + path: Array; + reason: Types.EvaluationErrorReason; + } + | { + __typename: 'FieldsNotDefinedConfigError'; + message: string; + path: Array; + reason: Types.EvaluationErrorReason; + } + | { + __typename: 'MissingFieldConfigError'; + message: string; + path: Array; + reason: Types.EvaluationErrorReason; + } + | { + __typename: 'MissingFieldsConfigError'; + message: string; + path: Array; + reason: Types.EvaluationErrorReason; + } + | { + __typename: 'RuntimeMismatchConfigError'; + message: string; + path: Array; + reason: Types.EvaluationErrorReason; + } + | { + __typename: 'SelectorTypeConfigError'; + message: string; + path: Array; + reason: Types.EvaluationErrorReason; + } + >; + } + | {__typename: 'RunConflict'} + | {__typename: 'UnauthorizedError'} + >; + } + | { + __typename: 'PythonError'; + message: string; + stack: Array; + errorChain: Array<{ + __typename: 'ErrorChainLink'; + isExplicitLink: boolean; + error: {__typename: 'PythonError'; message: string; stack: Array}; + }>; + }; +}; + export type DeleteMutationVariables = Types.Exact<{ runId: Types.Scalars['String']['input']; }>; @@ -168,6 +268,8 @@ export type RunTimeFragment = { export const LaunchPipelineExecutionVersion = '292088c4a697aca6be1d3bbc0cfc45d8a13cdb2e75cfedc64b68c6245ea34f89'; +export const LaunchMultipleRunsVersion = 'a56d9efdb35e71e0fd1744dd768129248943bc5b23e717458b82c46829661763'; + export const DeleteVersion = '3c61c79b99122910e754a8863e80dc5ed479a0c23cc1a9d9878d91e603fc0dfe'; export const TerminateVersion = '67acf403eb320a93c9a9aa07f675a1557e0887d499cd5598f1d5ff360afc15c0'; 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..533bc3e7c1cf1 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,6 +13,7 @@ import { Subheading, Tag, TextInput, + Tooltip, } from '@dagster-io/ui-components'; import {useCallback, useMemo, useState} from 'react'; import styled from 'styled-components'; @@ -31,15 +32,18 @@ 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 {SensorSelector} from '../graphql/types'; +import {useLaunchMultipleRunsWithTelemetry} from '../launchpad/useLaunchMultipleRunsWithTelemetry'; 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< +export type SensorDryRunInstigationTick = Extract< SensorDryRunMutation['sensorDryRun'], {__typename: 'DryRunInstigationTick'} >; @@ -76,12 +80,12 @@ 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 sensorSelector = useMemo( + const sensorSelector: SensorSelector = useMemo( () => ({ sensorName: name, repositoryLocationName: repoAddress.location, @@ -90,6 +94,14 @@ const SensorDryRun = ({repoAddress, name, currentCursor, onClose, jobName}: Prop [repoAddress, name], ); + const executionParamsList = useMemo( + () => + sensorExecutionData && sensorSelector + ? buildExecutionParamsListSensor(sensorExecutionData, sensorSelector) + : [], + [sensorSelector, sensorExecutionData], + ); + const submitTest = useCallback(async () => { setSubmitting(true); const result = await sensorDryRun({ @@ -120,11 +132,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 = useCallback(async () => { + if (!canLaunchAll) { + return; + } + setLaunching(true); + + try { + if (executionParamsList) { + await launchMultipleRunsWithTelemetry({executionParamsList}, 'toast'); + } + } catch (e) { + console.error(e); + } + + setLaunching(false); + onClose(); + }, [canLaunchAll, executionParamsList, launchMultipleRunsWithTelemetry, onClose]); + const buttons = useMemo(() => { + if (launching) { + return ; + } + if (sensorExecutionData || error) { return ( - + + +