Skip to content
Closed
2 changes: 2 additions & 0 deletions client/src/AppProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import AuthProvider from 'route/auth/AuthProvider';
import * as React from 'react';
import { Provider } from 'react-redux';
import store from 'store/store';
import ExecutionHistoryLoader from 'preview/components/execution/ExecutionHistoryLoader';

const mdTheme: Theme = createTheme({
palette: {
Expand All @@ -17,6 +18,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
<ThemeProvider theme={mdTheme}>
<AuthProvider>
<CssBaseline />
<ExecutionHistoryLoader />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this react component here?

{children}
</AuthProvider>
</ThemeProvider>
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all of the methods are returning Promise<void>. There are other possible outcomes with the IndexDB operations. Perhaps they should be used to provide meaningful promises?

Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import { DB_CONFIG, ExecutionHistoryEntry } from '../model/executionHistory';
import {
DB_CONFIG,
ExecutionHistoryEntry,
} from '../model/backend/gitlab/types/executionHistory';

/**
* Service for interacting with IndexedDB
* Interface for IndexedDB operations
*/
class IndexedDBService {
export interface IIndexedDBService {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the name is changed to IExecutionHistory, the ExecutionHistory suffix can be removed from the method names.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rest of the code need not know about the use of IndexDB. The interface can be independent of IndexDB and the ExecutionHistory class can include IndexDB inside without letting other parts of the code knowing about it.

init(): Promise<void>;
addExecutionHistory(entry: ExecutionHistoryEntry): Promise<string>;
updateExecutionHistory(entry: ExecutionHistoryEntry): Promise<void>;
getExecutionHistoryById(id: string): Promise<ExecutionHistoryEntry | null>;
getExecutionHistoryByDTName(dtName: string): Promise<ExecutionHistoryEntry[]>;
getAllExecutionHistory(): Promise<ExecutionHistoryEntry[]>;
deleteExecutionHistory(id: string): Promise<void>;
deleteExecutionHistoryByDTName(dtName: string): Promise<void>;
}

/**
* For interacting with IndexedDB
*/
class IndexedDBService implements IIndexedDBService {
private db: IDBDatabase | null = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be null.


private dbName: string;
Expand Down
23 changes: 23 additions & 0 deletions client/src/model/backend/gitlab/execution/interfaces.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code in src/model pure application logic and should not contain any UI components. The react code needs to be placed outside of src/model but in other sub-directories of src. The react code of DT execution can probably be placed in src/routes/digitaltwins/execution directory
The comment is true for all other files as well.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Dispatch, SetStateAction } from 'react';
import { ThunkDispatch, Action } from '@reduxjs/toolkit';
import { RootState } from 'store/store';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't import RootState here.

import DigitalTwin from 'preview/util/digitalTwin';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please do not import code from src/preview


export interface PipelineStatusParams {
setButtonText: Dispatch<SetStateAction<string>>;
digitalTwin: DigitalTwin;
setLogButtonDisabled: Dispatch<SetStateAction<boolean>>;
dispatch: ReturnType<typeof import('react-redux').useDispatch>;
executionId?: string;
}

export type PipelineHandlerDispatch = ThunkDispatch<
RootState,
unknown,
Action<string>
>;

export interface JobLog {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

has already been defined in src/model/backend/gitlab/types.ts

jobName: string;
log: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure that there are no circular dependencies

Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import { Dispatch, SetStateAction } from 'react';
import { useDispatch } from 'react-redux';
import DigitalTwin, { formatName } from 'preview/util/digitalTwin';
import indexedDBService from 'preview/services/indexedDBService';
import indexedDBService from 'database/digitalTwins';
import {
fetchJobLogs,
updatePipelineStateOnCompletion,
} from 'preview/route/digitaltwins/execute/pipelineUtils';
} from 'model/backend/gitlab/execution/pipelineUtils';
import { showSnackbar } from 'preview/store/snackbar.slice';
import { MAX_EXECUTION_TIME } from 'model/backend/gitlab/constants';
import { ExecutionStatus } from 'preview/model/executionHistory';
import { updateExecutionStatus } from 'preview/store/executionHistory.slice';

interface PipelineStatusParams {
setButtonText: Dispatch<SetStateAction<string>>;
digitalTwin: DigitalTwin;
setLogButtonDisabled: Dispatch<SetStateAction<boolean>>;
dispatch: ReturnType<typeof useDispatch>;
executionId?: string;
}
import { ExecutionStatus } from 'model/backend/gitlab/types/executionHistory';
import { updateExecutionStatus } from 'model/backend/gitlab/state/executionHistory.slice';
import {
setPipelineCompleted,
setPipelineLoading,
} from 'model/backend/gitlab/state/digitalTwin.slice';
import { PipelineStatusParams } from './interfaces';

export const delay = (ms: number) =>
new Promise((resolve) => {
Expand Down Expand Up @@ -136,18 +133,67 @@ export const handlePipelineCompletion = async (
pipelineStatus: 'success' | 'failed',
executionId?: string,
) => {
const jobLogs = await fetchJobLogs(digitalTwin.gitlabInstance, pipelineId);
await updatePipelineStateOnCompletion(
digitalTwin,
jobLogs,
setButtonText,
setLogButtonDisabled,
dispatch,
executionId,
const status =
pipelineStatus === 'success'
? ExecutionStatus.COMPLETED
: ExecutionStatus.FAILED,
);
: ExecutionStatus.FAILED;

if (!executionId) {
// For backward compatibility
const jobLogs = await fetchJobLogs(digitalTwin.gitlabInstance, pipelineId);
await updatePipelineStateOnCompletion(
digitalTwin,
jobLogs,
setButtonText,
setLogButtonDisabled,
dispatch,
undefined,
status,
);
} else {
// For concurrent executions, use the new helper function
const { fetchLogsAndUpdateExecution } = await import('./pipelineUtils');

// Fetch logs and update execution
const logsUpdated = await fetchLogsAndUpdateExecution(
digitalTwin,
pipelineId,
executionId,
status,
dispatch,
);

if (!logsUpdated) {
await delay(5000);
await handlePipelineCompletion(
pipelineId,
digitalTwin,
setButtonText,
setLogButtonDisabled,
dispatch,
pipelineStatus,
executionId,
);
return;
}

setButtonText('Start');
setLogButtonDisabled(false);

// For backward compatibility
dispatch(
setPipelineCompleted({
assetName: digitalTwin.DTName,
pipelineCompleted: true,
}),
);
dispatch(
setPipelineLoading({
assetName: digitalTwin.DTName,
pipelineLoading: false,
}),
);
}

if (pipelineStatus === 'failed') {
dispatch(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Dispatch, SetStateAction } from 'react';
import DigitalTwin, { formatName } from 'preview/util/digitalTwin';
import { useDispatch } from 'react-redux';
import { showSnackbar } from 'preview/store/snackbar.slice';
import { fetchExecutionHistory } from 'model/backend/gitlab/state/executionHistory.slice';
import {
startPipeline,
updatePipelineState,
updatePipelineStateOnStop,
} from './pipelineUtils';
import { startPipelineStatusCheck } from './pipelineChecks';
import { PipelineHandlerDispatch } from './interfaces';

export const handleButtonClick = (
buttonText: string,
setButtonText: Dispatch<SetStateAction<string>>,
digitalTwin: DigitalTwin,
setLogButtonDisabled: Dispatch<SetStateAction<boolean>>,
dispatch: ReturnType<typeof useDispatch>,
dispatch: PipelineHandlerDispatch,
) => {
if (buttonText === 'Start') {
handleStart(
Expand All @@ -34,7 +35,7 @@ export const handleStart = async (
setButtonText: Dispatch<SetStateAction<string>>,
digitalTwin: DigitalTwin,
setLogButtonDisabled: Dispatch<SetStateAction<boolean>>,
dispatch: ReturnType<typeof useDispatch>,
dispatch: PipelineHandlerDispatch,
executionId?: string,
) => {
if (buttonText === 'Start') {
Expand All @@ -49,6 +50,8 @@ export const handleStart = async (
);

if (newExecutionId) {
dispatch(fetchExecutionHistory(digitalTwin.DTName));

const params = {
setButtonText,
digitalTwin,
Expand All @@ -72,7 +75,7 @@ export const handleStart = async (
export const handleStop = async (
digitalTwin: DigitalTwin,
setButtonText: Dispatch<SetStateAction<string>>,
dispatch: ReturnType<typeof useDispatch>,
dispatch: PipelineHandlerDispatch,
executionId?: string,
) => {
try {
Expand Down Expand Up @@ -119,7 +122,7 @@ export const stopPipelines = async (
executionId,
);
} else if (digitalTwin.pipelineId) {
// For backward compatibility, stop the current execution
// backward compatibility, stop the current execution
await digitalTwin.stop(
digitalTwin.gitlabInstance.projectId,
'parentPipeline',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,22 @@ import {
setJobLogs,
setPipelineCompleted,
setPipelineLoading,
} from 'preview/store/digitalTwin.slice';
} from 'model/backend/gitlab/state/digitalTwin.slice';
import { useDispatch } from 'react-redux';
import { showSnackbar } from 'preview/store/snackbar.slice';
import { ExecutionStatus, JobLog } from 'preview/model/executionHistory';
import { ExecutionStatus } from 'model/backend/gitlab/types/executionHistory';
import {
updateExecutionLogs,
updateExecutionStatus,
setSelectedExecutionId,
} from 'preview/store/executionHistory.slice';

/**
* Start a pipeline execution and create an execution history entry
* @param digitalTwin The Digital Twin to execute
* @param dispatch Redux dispatch function
* @param setLogButtonDisabled Function to set the log button disabled state
* @returns The execution ID
*/
} from 'model/backend/gitlab/state/executionHistory.slice';
import { JobLog } from './interfaces';

export const delay = (ms: number) =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});

export const startPipeline = async (
digitalTwin: DigitalTwin,
dispatch: ReturnType<typeof useDispatch>,
Expand Down Expand Up @@ -56,12 +55,6 @@ export const startPipeline = async (
return digitalTwin.currentExecutionId;
};

/**
* Update the pipeline state in the Redux store
* @param digitalTwin The Digital Twin
* @param dispatch Redux dispatch function
* @param executionId Optional execution ID for concurrent executions
*/
export const updatePipelineState = (
digitalTwin: DigitalTwin,
dispatch: ReturnType<typeof useDispatch>,
Expand Down Expand Up @@ -91,16 +84,6 @@ export const updatePipelineState = (
}
};

/**
* Update the pipeline state on completion
* @param digitalTwin The Digital Twin
* @param jobLogs The job logs
* @param setButtonText Function to set the button text
* @param setLogButtonDisabled Function to set the log button disabled state
* @param dispatch Redux dispatch function
* @param executionId Optional execution ID for concurrent executions
* @param status Optional status for the execution
*/
export const updatePipelineStateOnCompletion = async (
digitalTwin: DigitalTwin,
jobLogs: JobLog[],
Expand Down Expand Up @@ -146,13 +129,6 @@ export const updatePipelineStateOnCompletion = async (
setButtonText('Start');
};

/**
* Update the pipeline state on stop
* @param digitalTwin The Digital Twin
* @param setButtonText Function to set the button text
* @param dispatch Redux dispatch function
* @param executionId Optional execution ID for concurrent executions
*/
export const updatePipelineStateOnStop = (
digitalTwin: DigitalTwin,
setButtonText: Dispatch<SetStateAction<string>>,
Expand Down Expand Up @@ -186,12 +162,6 @@ export const updatePipelineStateOnStop = (
}
};

/**
* Fetch job logs from GitLab
* @param gitlabInstance The GitLab instance
* @param pipelineId The pipeline ID
* @returns Promise that resolves with an array of job logs
*/
export const fetchJobLogs = async (
gitlabInstance: GitlabInstance,
pipelineId: number,
Expand Down Expand Up @@ -230,3 +200,44 @@ export const fetchJobLogs = async (
});
return (await Promise.all(logPromises)).reverse();
};

export const fetchLogsAndUpdateExecution = async (
digitalTwin: DigitalTwin,
pipelineId: number,
executionId: string,
status: ExecutionStatus,
dispatch: ReturnType<typeof useDispatch>,
): Promise<boolean> => {
try {
const jobLogs = await fetchJobLogs(digitalTwin.gitlabInstance, pipelineId);

if (
jobLogs.length === 0 ||
jobLogs.every((log) => !log.log || log.log.trim() === '')
) {
return false;
}

await digitalTwin.updateExecutionLogs(executionId, jobLogs);

await digitalTwin.updateExecutionStatus(executionId, status);

dispatch(
updateExecutionLogs({
id: executionId,
logs: jobLogs,
}),
);

dispatch(
updateExecutionStatus({
id: executionId,
status,
}),
);

return true;
} catch (_error) {
return false;
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The coding idioms in src/store are good. Please attempt to follow them if possible.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import DigitalTwin from 'preview/util/digitalTwin';
import { JobLog } from 'preview/model/executionHistory';
import { JobLog } from 'model/backend/gitlab/types/executionHistory';
import { RootState } from 'store/store';

interface DigitalTwinState {
Expand Down Expand Up @@ -72,6 +72,9 @@ const digitalTwinSlice = createSlice({
export const selectDigitalTwinByName = (name: string) => (state: RootState) =>
state.digitalTwin.digitalTwin[name];

export const selectDigitalTwins = (state: RootState) =>
Object.values(state.digitalTwin.digitalTwin);

export const selectShouldFetchDigitalTwins = (state: RootState) =>
state.digitalTwin.shouldFetchDigitalTwins;

Expand Down
Loading
Loading