-
Notifications
You must be signed in to change notification settings - Fork 69
feat: Concurrent executions with indexedDB #1235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
26e5724
e168a9a
6fc0304
117405f
a8a6614
047f00d
b91218f
58f6d06
c80a929
9d3f7e3
4e522fe
1c0f14c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Microchesst marked this conversation as resolved.
Show resolved
Hide resolved
Microchesst marked this conversation as resolved.
Show resolved
Hide resolved
Microchesst marked this conversation as resolved.
Show resolved
Hide resolved
|
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file can be placed in |
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,14 +1,11 @@ | ||||||||||||||||
| import * as React from 'react'; | ||||||||||||||||
| import { useState, Dispatch, SetStateAction } from 'react'; | ||||||||||||||||
| import { Button, CircularProgress } from '@mui/material'; | ||||||||||||||||
| import { handleButtonClick } from 'preview/route/digitaltwins/execute/pipelineHandler'; | ||||||||||||||||
| import { Dispatch, SetStateAction } from 'react'; | ||||||||||||||||
| import { Button, CircularProgress, Box } from '@mui/material'; | ||||||||||||||||
| import { handleStart } from 'preview/route/digitaltwins/execute/pipelineHandler'; | ||||||||||||||||
| import { useSelector, useDispatch } from 'react-redux'; | ||||||||||||||||
| import { selectDigitalTwinByName } from 'preview/store/digitalTwin.slice'; | ||||||||||||||||
|
|
||||||||||||||||
| export interface JobLog { | ||||||||||||||||
| jobName: string; | ||||||||||||||||
| log: string; | ||||||||||||||||
| } | ||||||||||||||||
| import { selectExecutionHistoryByDTName } from 'preview/store/executionHistory.slice'; | ||||||||||||||||
| import { ExecutionStatus } from 'preview/model/executionHistory'; | ||||||||||||||||
|
|
||||||||||||||||
| interface StartStopButtonProps { | ||||||||||||||||
| assetName: string; | ||||||||||||||||
|
|
@@ -19,33 +16,52 @@ function StartStopButton({ | |||||||||||||||
| assetName, | ||||||||||||||||
| setLogButtonDisabled, | ||||||||||||||||
| }: StartStopButtonProps) { | ||||||||||||||||
| const [buttonText, setButtonText] = useState('Start'); | ||||||||||||||||
|
|
||||||||||||||||
| const dispatch = useDispatch(); | ||||||||||||||||
| const digitalTwin = useSelector(selectDigitalTwinByName(assetName)); | ||||||||||||||||
| const executions = | ||||||||||||||||
| useSelector(selectExecutionHistoryByDTName(assetName)) || []; | ||||||||||||||||
|
|
||||||||||||||||
| const runningExecutions = Array.isArray(executions) | ||||||||||||||||
| ? executions.filter( | ||||||||||||||||
| (execution) => execution.status === ExecutionStatus.RUNNING, | ||||||||||||||||
| ) | ||||||||||||||||
| : []; | ||||||||||||||||
|
|
||||||||||||||||
| const isLoading = | ||||||||||||||||
| digitalTwin?.pipelineLoading || runningExecutions.length > 0; | ||||||||||||||||
|
|
||||||||||||||||
| const runningCount = runningExecutions.length; | ||||||||||||||||
|
|
||||||||||||||||
| return ( | ||||||||||||||||
| <> | ||||||||||||||||
| {digitalTwin?.pipelineLoading ? ( | ||||||||||||||||
| <CircularProgress size={22} style={{ marginRight: '8px' }} /> | ||||||||||||||||
| ) : null} | ||||||||||||||||
| <Box display="flex" alignItems="center"> | ||||||||||||||||
| {isLoading && ( | ||||||||||||||||
| <Box display="flex" alignItems="center" mr={1}> | ||||||||||||||||
| <CircularProgress size={22} data-testid="circular-progress" /> | ||||||||||||||||
| {runningCount > 0 && ( | ||||||||||||||||
| <Box component="span" ml={0.5} fontSize="0.75rem"> | ||||||||||||||||
| ({runningCount}) | ||||||||||||||||
| </Box> | ||||||||||||||||
| )} | ||||||||||||||||
| </Box> | ||||||||||||||||
| )} | ||||||||||||||||
| <Button | ||||||||||||||||
| variant="contained" | ||||||||||||||||
| size="small" | ||||||||||||||||
| color="primary" | ||||||||||||||||
| onClick={() => | ||||||||||||||||
| handleButtonClick( | ||||||||||||||||
| buttonText, | ||||||||||||||||
| onClick={() => { | ||||||||||||||||
| const setButtonText = () => {}; // Dummy function since we don't need to change button text | ||||||||||||||||
| handleStart( | ||||||||||||||||
| 'Start', | ||||||||||||||||
|
||||||||||||||||
| const setButtonText = () => {}; // Dummy function since we don't need to change button text | |
| handleStart( | |
| 'Start', | |
| const newButtonText = buttonText === 'Start' ? 'Stop' : 'Start'; | |
| setButtonText(newButtonText); | |
| handleStart( | |
| newButtonText, |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please move this to |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| import * as React from 'react'; | ||
| import { useEffect } from 'react'; | ||
| import { useDispatch, useSelector } from 'react-redux'; | ||
| import { | ||
| List, | ||
| ListItem, | ||
| ListItemText, | ||
| IconButton, | ||
| Typography, | ||
| Paper, | ||
| Box, | ||
| Tooltip, | ||
| CircularProgress, | ||
| Divider, | ||
| } from '@mui/material'; | ||
| import { | ||
| Delete as DeleteIcon, | ||
| Visibility as VisibilityIcon, | ||
| CheckCircle as CheckCircleIcon, | ||
| Error as ErrorIcon, | ||
| Cancel as CancelIcon, | ||
| AccessTime as AccessTimeIcon, | ||
| HourglassEmpty as HourglassEmptyIcon, | ||
| Stop as StopIcon, | ||
| } from '@mui/icons-material'; | ||
| import { ExecutionStatus } from 'preview/model/executionHistory'; | ||
| import { | ||
| fetchExecutionHistory, | ||
| removeExecution, | ||
| selectExecutionHistoryByDTName, | ||
| selectExecutionHistoryLoading, | ||
| setSelectedExecutionId, | ||
| } from 'preview/store/executionHistory.slice'; | ||
| import { selectDigitalTwinByName } from 'preview/store/digitalTwin.slice'; | ||
| import { handleStop } from 'preview/route/digitaltwins/execute/pipelineHandler'; | ||
| import { ThunkDispatch, Action } from '@reduxjs/toolkit'; | ||
| import { RootState } from 'store/store'; | ||
|
|
||
| interface ExecutionHistoryListProps { | ||
| dtName: string; | ||
| onViewLogs: (executionId: string) => void; | ||
| } | ||
|
|
||
| const formatTimestamp = (timestamp: number): string => { | ||
| const date = new Date(timestamp); | ||
| return date.toLocaleString(); | ||
| }; | ||
|
|
||
| const getStatusIcon = (status: ExecutionStatus) => { | ||
| switch (status) { | ||
| case ExecutionStatus.COMPLETED: | ||
| return <CheckCircleIcon color="success" />; | ||
| case ExecutionStatus.FAILED: | ||
| return <ErrorIcon color="error" />; | ||
| case ExecutionStatus.CANCELED: | ||
| return <CancelIcon color="warning" />; | ||
| case ExecutionStatus.TIMEOUT: | ||
| return <HourglassEmptyIcon color="warning" />; | ||
| case ExecutionStatus.RUNNING: | ||
| return <CircularProgress size={20} />; | ||
| default: | ||
| return <AccessTimeIcon />; | ||
| } | ||
| }; | ||
|
|
||
| const getStatusText = (status: ExecutionStatus): string => { | ||
| switch (status) { | ||
| case ExecutionStatus.COMPLETED: | ||
| return 'Completed'; | ||
| case ExecutionStatus.FAILED: | ||
| return 'Failed'; | ||
| case ExecutionStatus.CANCELED: | ||
| return 'Canceled'; | ||
| case ExecutionStatus.TIMEOUT: | ||
| return 'Timed out'; | ||
| case ExecutionStatus.RUNNING: | ||
| return 'Running'; | ||
| default: | ||
| return 'Unknown'; | ||
| } | ||
| }; | ||
|
|
||
| const ExecutionHistoryList: React.FC<ExecutionHistoryListProps> = ({ | ||
| dtName, | ||
| onViewLogs, | ||
| }) => { | ||
| // Use typed dispatch for thunk actions | ||
| const dispatch = | ||
| useDispatch<ThunkDispatch<RootState, unknown, Action<string>>>(); | ||
| const executions = useSelector(selectExecutionHistoryByDTName(dtName)); | ||
| const loading = useSelector(selectExecutionHistoryLoading); | ||
| const digitalTwin = useSelector(selectDigitalTwinByName(dtName)); | ||
|
|
||
| useEffect(() => { | ||
| // Use the thunk action creator directly | ||
| dispatch(fetchExecutionHistory(dtName)); | ||
| }, [dispatch, dtName]); | ||
|
|
||
| const handleDelete = (executionId: string) => { | ||
| // Use the thunk action creator directly | ||
| dispatch(removeExecution(executionId)); | ||
| }; | ||
|
|
||
| const handleViewLogs = (executionId: string) => { | ||
| dispatch(setSelectedExecutionId(executionId)); | ||
| onViewLogs(executionId); | ||
| }; | ||
|
|
||
| const handleStopExecution = (executionId: string) => { | ||
| if (digitalTwin) { | ||
| // Dummy function since we don't need to change button text | ||
| const setButtonText = () => {}; | ||
| handleStop(digitalTwin, setButtonText, dispatch, executionId); | ||
| } | ||
| }; | ||
|
|
||
| if (loading) { | ||
| return ( | ||
| <Box display="flex" justifyContent="center" alignItems="center" p={2}> | ||
| <div data-testid="circular-progress"> | ||
| <CircularProgress data-testid="progress-indicator" /> | ||
| </div> | ||
| </Box> | ||
| ); | ||
| } | ||
|
|
||
| if (!executions || executions.length === 0) { | ||
| return ( | ||
| <Paper elevation={2} sx={{ p: 2, mt: 2 }}> | ||
| <Typography variant="body1" align="center"> | ||
| No execution history found. Start an execution to see it here. | ||
| </Typography> | ||
| </Paper> | ||
| ); | ||
| } | ||
|
|
||
| const sortedExecutions = [...executions].sort( | ||
| (a, b) => b.timestamp - a.timestamp, | ||
| ); | ||
|
|
||
| return ( | ||
| <Paper elevation={2} sx={{ mt: 2 }}> | ||
| <Box p={2}> | ||
| <Typography variant="h6" gutterBottom> | ||
| Execution History | ||
| </Typography> | ||
| <List> | ||
| {sortedExecutions.map((execution) => ( | ||
| <React.Fragment key={execution.id}> | ||
| <ListItem | ||
| secondaryAction={ | ||
| <Box display="flex"> | ||
| <Tooltip title="View Logs"> | ||
| <IconButton | ||
| edge="end" | ||
| aria-label="view" | ||
| onClick={() => handleViewLogs(execution.id)} | ||
| > | ||
| <VisibilityIcon /> | ||
| </IconButton> | ||
| </Tooltip> | ||
| {execution.status === ExecutionStatus.RUNNING && ( | ||
| <Tooltip title="Stop Execution"> | ||
| <IconButton | ||
| edge="end" | ||
| aria-label="stop" | ||
| onClick={() => handleStopExecution(execution.id)} | ||
| > | ||
| <StopIcon /> | ||
| </IconButton> | ||
| </Tooltip> | ||
| )} | ||
| <Tooltip title="Delete"> | ||
| <IconButton | ||
| edge="end" | ||
| aria-label="delete" | ||
| onClick={() => handleDelete(execution.id)} | ||
| > | ||
| <DeleteIcon /> | ||
| </IconButton> | ||
| </Tooltip> | ||
| </Box> | ||
| } | ||
| > | ||
| <Box display="flex" alignItems="center" mr={2}> | ||
| {getStatusIcon(execution.status)} | ||
| </Box> | ||
| <ListItemText | ||
| primary={formatTimestamp(execution.timestamp)} | ||
| secondary={ | ||
| <Typography variant="body2" color="textSecondary"> | ||
| Status: {getStatusText(execution.status)} | ||
| </Typography> | ||
| } | ||
| /> | ||
| </ListItem> | ||
| <Divider variant="inset" component="li" /> | ||
| </React.Fragment> | ||
| ))} | ||
| </List> | ||
| </Box> | ||
| </Paper> | ||
| ); | ||
| }; | ||
|
|
||
| export default ExecutionHistoryList; | ||
Uh oh!
There was an error while loading. Please reload this page.