Skip to content
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"@types/react-syntax-highlighter": "^15.5.13",
"@types/remarkable": "^2.0.8",
"@types/styled-components": "^5.1.32",
"@types/uuid": "10.0.0",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
Expand Down Expand Up @@ -102,6 +103,7 @@
"serve": "^14.2.1",
"styled-components": "^6.1.1",
"typescript": "5.1.6",
"uuid": "11.1.0",
"zod": "3.24.1"
},
"devDependencies": {
Expand All @@ -121,6 +123,7 @@
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"eslint-config-react-app": "^7.0.1",
"fake-indexeddb": "6.0.1",
"globals": "15.11.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "29.7.0",
Expand Down
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
302 changes: 302 additions & 0 deletions client/src/database/digitalTwins.ts
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
@@ -0,0 +1,302 @@
import {
DB_CONFIG,
ExecutionHistoryEntry,
} from '../model/backend/gitlab/types/executionHistory';

/**
* Interface for IndexedDB operations
*/
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;

private dbVersion: number;

constructor() {
this.dbName = DB_CONFIG.name;
this.dbVersion = DB_CONFIG.version;
}

/**
* Initialize the database
* @returns Promise that resolves when the database is initialized
*/
public async init(): Promise<void> {
if (this.db) {
return Promise.resolve();
}

return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);

request.onerror = () => {
reject(new Error('Failed to open IndexedDB'));
};

request.onsuccess = (event) => {
this.db = (event.target as IDBOpenDBRequest).result;
resolve();
};

request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;

// Create object stores and indexes
if (!db.objectStoreNames.contains('executionHistory')) {
const store = db.createObjectStore('executionHistory', {
keyPath: DB_CONFIG.stores.executionHistory.keyPath,
});

// Create indexes
DB_CONFIG.stores.executionHistory.indexes.forEach((index) => {
store.createIndex(index.name, index.keyPath);
});
}
};
});
}

/**
* Add a new execution history entry
* @param entry The execution history entry to add
* @returns Promise that resolves with the ID of the added entry
*/
public async addExecutionHistory(
entry: ExecutionHistoryEntry,
): Promise<string> {
await this.init();

return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not initialized'));
return;
}

const transaction = this.db.transaction(
['executionHistory'],
'readwrite',
);
const store = transaction.objectStore('executionHistory');
const request = store.add(entry);

request.onerror = () => {
reject(new Error('Failed to add execution history'));
};

request.onsuccess = () => {
resolve(entry.id);
};
});
}

/**
* Update an existing execution history entry
* @param entry The execution history entry to update
* @returns Promise that resolves when the entry is updated
*/
public async updateExecutionHistory(
entry: ExecutionHistoryEntry,
): Promise<void> {
await this.init();

return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not initialized'));
return;
}

const transaction = this.db.transaction(
['executionHistory'],
'readwrite',
);
const store = transaction.objectStore('executionHistory');
const request = store.put(entry);

request.onerror = () => {
reject(new Error('Failed to update execution history'));
};

request.onsuccess = () => {
resolve();
};
});
}

/**
* Get an execution history entry by ID
* @param id The ID of the execution history entry
* @returns Promise that resolves with the execution history entry
*/
public async getExecutionHistoryById(
id: string,
): Promise<ExecutionHistoryEntry | null> {
await this.init();

return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not initialized'));
return;
}

const transaction = this.db.transaction(['executionHistory'], 'readonly');
const store = transaction.objectStore('executionHistory');
const request = store.get(id);

request.onerror = () => {
reject(new Error('Failed to get execution history'));
};

request.onsuccess = () => {
resolve(request.result || null);
};
});
}

/**
* Get all execution history entries for a Digital Twin
* @param dtName The name of the Digital Twin
* @returns Promise that resolves with an array of execution history entries
*/
public async getExecutionHistoryByDTName(
dtName: string,
): Promise<ExecutionHistoryEntry[]> {
await this.init();

return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not initialized'));
return;
}

const transaction = this.db.transaction(['executionHistory'], 'readonly');
const store = transaction.objectStore('executionHistory');
const index = store.index('dtName');
const request = index.getAll(dtName);

request.onerror = () => {
reject(new Error('Failed to get execution history by DT name'));
};

request.onsuccess = () => {
resolve(request.result || []);
};
});
}

/**
* Get all execution history entries
* @returns Promise that resolves with an array of all execution history entries
*/
public async getAllExecutionHistory(): Promise<ExecutionHistoryEntry[]> {
await this.init();

return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not initialized'));
return;
}

const transaction = this.db.transaction(['executionHistory'], 'readonly');
const store = transaction.objectStore('executionHistory');
const request = store.getAll();

request.onerror = () => {
reject(new Error('Failed to get all execution history'));
};

request.onsuccess = () => {
resolve(request.result || []);
};
});
}

/**
* Delete an execution history entry
* @param id The ID of the execution history entry to delete
* @returns Promise that resolves when the entry is deleted
*/
public async deleteExecutionHistory(id: string): Promise<void> {
await this.init();

return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not initialized'));
return;
}

const transaction = this.db.transaction(
['executionHistory'],
'readwrite',
);
const store = transaction.objectStore('executionHistory');
const request = store.delete(id);

request.onerror = () => {
reject(new Error('Failed to delete execution history'));
};

request.onsuccess = () => {
resolve();
};
});
}

/**
* Delete all execution history entries for a Digital Twin
* @param dtName The name of the Digital Twin
* @returns Promise that resolves when all entries are deleted
*/
public async deleteExecutionHistoryByDTName(dtName: string): Promise<void> {
await this.init();

return new Promise((resolve, reject) => {
if (!this.db) {
reject(new Error('Database not initialized'));
return;
}

const transaction = this.db.transaction(
['executionHistory'],
'readwrite',
);
const store = transaction.objectStore('executionHistory');
const index = store.index('dtName');
const request = index.openCursor(IDBKeyRange.only(dtName));

request.onerror = () => {
reject(new Error('Failed to delete execution history by DT name'));
};

request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result;
if (cursor) {
cursor.delete();
cursor.continue();
} else {
resolve();
}
};
});
}
}

// Create a singleton instance
const indexedDBService = new IndexedDBService();

// Export the singleton instance as default
export default indexedDBService;
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;
}
Loading
Loading