/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { forwardRef, useEffect } from 'react'; import { observer } from 'mobx-react-lite'; import { type TreeData, type TreeNodeContainerProps, clsx, Dialog, MenuContent, MenuContentItem, PanelLoadingIndicator, TreeView, ContextMenu, ChevronDownIcon, ChevronRightIcon, RefreshIcon, CircleNotchIcon, QuestionCircleIcon, TimesCircleIcon, CheckCircleIcon, PauseCircleIcon, BanIcon, PanelContent, ModalBody, ModalFooter, ModalHeader, ModalHeaderActions, ModalTitle, Modal, ModalFooterButton, } from '@finos/legend-art'; import { LEGEND_STUDIO_TEST_ID } from '../../../__lib__/LegendStudioTesting.js'; import { flowResult } from 'mobx'; import { WorkflowJobStatus, WorkflowStatus } from '@finos/legend-server-sdlc'; import { useApplicationStore } from '@finos/legend-application'; import { type WorkflowExplorerTreeNodeData, type WorkflowLogState, type WorkflowManagerState, type WorkflowState, WorkflowJobTreeNodeData, WorkflowTreeNodeData, } from '../../../stores/editor/sidebar-state/WorkflowManagerState.js'; import { formatDistanceToNow, guaranteeNonNullable, guaranteeType, isNonNullable, } from '@finos/legend-shared'; import { CODE_EDITOR_LANGUAGE } from '@finos/legend-code-editor'; import { CodeEditor } from '@finos/legend-lego/code-editor'; import { LegendStudioTelemetryHelper } from '../../../__lib__/LegendStudioTelemetryHelper.js'; const getWorkflowStatusIcon = ( workflowStatus: WorkflowStatus, ): React.ReactNode => { switch (workflowStatus) { case WorkflowStatus.PENDING: return (
); case WorkflowStatus.IN_PROGRESS: return (
); case WorkflowStatus.SUCCEEDED: return (
); case WorkflowStatus.FAILED: return (
); case WorkflowStatus.CANCELED: return (
); case WorkflowStatus.UNKNOWN: default: return (
); } }; const getWorkflowJobStatusIcon = ( workflowStatus: WorkflowJobStatus, ): React.ReactNode => { switch (workflowStatus) { case WorkflowJobStatus.WAITING: case WorkflowJobStatus.WAITING_MANUAL: return (
); case WorkflowJobStatus.IN_PROGRESS: return (
); case WorkflowJobStatus.SUCCEEDED: return (
); case WorkflowJobStatus.FAILED: return (
); case WorkflowJobStatus.CANCELED: return (
); case WorkflowJobStatus.UNKNOWN: default: return (
); } }; const WorkflowJobLogsViewer = observer( (props: { workflowState: WorkflowManagerState; logState: WorkflowLogState; }) => { const { workflowState, logState } = props; const applicationStore = workflowState.editorStore.applicationStore; const job = guaranteeNonNullable(logState.job); const jobIsInProgress = job.status === WorkflowJobStatus.IN_PROGRESS; const closeLogViewer = (): void => { logState.closeModal(); flowResult(workflowState.fetchAllWorkflows()).catch( workflowState.editorStore.applicationStore.alertUnhandledError, ); }; const refreshLogs = (): void => { logState.refreshJobLogs(job); }; const logs = logState.logs; return ( ); }, ); const WorkflowExplorerContextMenu = observer( forwardRef< HTMLDivElement, { workflowManagerState: WorkflowManagerState; workflowState: WorkflowState; node: WorkflowExplorerTreeNodeData; treeData: TreeData; } >(function WorkflowExplorerContextMenu(props, ref) { const { node, workflowManagerState, workflowState, treeData } = props; const applicationStore = useApplicationStore(); const retryJob = (): void => { if (node instanceof WorkflowJobTreeNodeData) { workflowState.retryJob(node.workflowJob, treeData); } }; const cancelJob = (): void => { if (node instanceof WorkflowJobTreeNodeData) { workflowState.cancelJob(node.workflowJob, treeData); } }; const runManualJob = (): void => { if (node instanceof WorkflowJobTreeNodeData) { workflowState.runManualJob(node.workflowJob, treeData); } }; const viewLogs = (): void => { if (node instanceof WorkflowJobTreeNodeData) { workflowManagerState.logState.viewJobLogs(node.workflowJob); } }; const visitWeburl = (): void => { if (node instanceof WorkflowJobTreeNodeData) { applicationStore.navigationService.navigator.visitAddress( node.workflowJob.webURL, ); } else if (node instanceof WorkflowTreeNodeData) { applicationStore.navigationService.navigator.visitAddress( node.workflow.webURL, ); } }; return ( {node instanceof WorkflowJobTreeNodeData && ( <> View Logs Visit Job {node.workflowJob.status !== WorkflowJobStatus.IN_PROGRESS && ( Retry Job )} {node.workflowJob.status === WorkflowJobStatus.IN_PROGRESS && ( Cancel Job )} {node.workflowJob.status === WorkflowJobStatus.WAITING_MANUAL && ( Run Manual Job )} )} {node instanceof WorkflowTreeNodeData && ( Visit Workflow )} ); }), ); const WorkflowTreeNodeContainer: React.FC< TreeNodeContainerProps< WorkflowExplorerTreeNodeData, { workflowManagerState: WorkflowManagerState; workflowState: WorkflowState; treeData: TreeData; } > > = (props) => { const { node, level, stepPaddingInRem, onNodeSelect } = props; const { workflowManagerState, treeData, workflowState } = props.innerProps; const expandIcon = !(node instanceof WorkflowTreeNodeData) ? (
) : node.isOpen ? ( ) : ( ); const nodeIcon = node instanceof WorkflowTreeNodeData ? getWorkflowStatusIcon(node.workflow.status) : getWorkflowJobStatusIcon( guaranteeType(node, WorkflowJobTreeNodeData).workflowJob.status, ); const selectNode: React.MouseEventHandler = (event) => onNodeSelect?.(node); return ( } menuProps={{ elevation: 7 }} > ); }; export const WorkflowManager = observer( (props: { workflowManagerState: WorkflowManagerState }) => { const applicationStore = useApplicationStore(); const workflowManagerState = props.workflowManagerState; const isDispatchingAction = workflowManagerState.fetchWorkflowsState.isInProgress || Boolean( workflowManagerState.workflowStates.find( (e) => e.isExecutingWorkflowRequest, ), ); const renderWorkflowContent = (): React.ReactNode => ( <> {workflowManagerState.workflowStates.map((workflowState) => { const onNodeSelect = (node: WorkflowExplorerTreeNodeData): void => { workflowState.onTreeNodeSelect(node, workflowState.treeData); }; const getChildNodes = ( node: WorkflowExplorerTreeNodeData, ): WorkflowExplorerTreeNodeData[] => { if (node.childrenIds && node instanceof WorkflowTreeNodeData) { return node.childrenIds .map((id) => workflowState.treeData.nodes.get(id)) .filter(isNonNullable); } return []; }; return ( ); })} {workflowManagerState.logState.job && ( )} ); const refresh = applicationStore.guardUnhandledError(() => flowResult(workflowManagerState.fetchAllWorkflows()), ); useEffect(() => { flowResult(workflowManagerState.fetchAllWorkflows()).catch( applicationStore.alertUnhandledError, ); }, [applicationStore, workflowManagerState]); // Panel open / close telemetry — anchors a dwell time so we can measure // engagement per surface. The mount emits `panel.open` and the cleanup // emits `panel.close` with `dwellMs`. useEffect(() => { const openedAt = Date.now(); const editorStore = workflowManagerState.editorStore; LegendStudioTelemetryHelper.logEvent_WorkflowManagerPanelOpened( editorStore.applicationStore.telemetryService, editorStore.editorMode.getSourceInfo(), { scope: workflowManagerState.scope }, ); return () => { LegendStudioTelemetryHelper.logEvent_WorkflowManagerPanelClosed( editorStore.applicationStore.telemetryService, editorStore.editorMode.getSourceInfo(), { scope: workflowManagerState.scope, dwellMs: Date.now() - openedAt, }, ); }; }, [workflowManagerState]); return (
WORKFLOW MANAGER
WORKFLOWS
{workflowManagerState.workflowStates.length}
{renderWorkflowContent()}
); }, );