Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
379 changes: 184 additions & 195 deletions components/log-viewer-webui/client/package-lock.json

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion components/log-viewer-webui/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@
"antd": "^5.24.5",
"axios": "^1.7.9",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"socket.io-client": "^4.8.1"
},
"devDependencies": {
"@eslint/js": "^9.19.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@types/socket.io-client": "^3.0.0",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.19.0",
"eslint-config-yscope": "latest",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"typescript": "~5.6.2",
"vite": "^6.2.5"
}
Expand Down
61 changes: 55 additions & 6 deletions components/log-viewer-webui/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,68 @@
import {CssVarsProvider} from "@mui/joy";
/* eslint-disable react/jsx-key */

import {LOCAL_STORAGE_KEY} from "./typings/config";
import QueryStatus from "./ui/QueryStatus";
// import { CssVarsProvider } from "@mui/joy";
// import { LOCAL_STORAGE_KEY } from "./typings/config";
// import QueryStatus from "./ui/QueryStatus";
import MongoCollection from "./mongoCDCLib/MongoCollection";
import {useTracker} from "./mongoCDCLib/useTracker";


const collection = new MongoCollection("compression-jobs");

// const collection2 = new MongoReplicaCollection("compression-jobs");

/**
* Renders the main application.
*
* @return
*/
const App = () => {
const results = useTracker(
() => collection.find({
_id: {
$gte: 1,
$lte: 10,
},
}, {sort: {start_time: -1}}),
[]
);

const results2 = useTracker(
() => collection.find({
_id: {
$gte: 1,
$lte: 5,
},
}, {sort: {start_time: -1}}),
[]
);

/* for (let i = 0; i < results.length; i++) {
console.log(results[i]);
}

for (let i = 0; i < results2.length; i++) {
console.log(results2[i]);
} */

return (
<CssVarsProvider modeStorageKey={LOCAL_STORAGE_KEY.THEME}>
<QueryStatus/>
</CssVarsProvider>
<div>
{results.map((r) => (
<div>
{JSON.stringify(r)}
</div>
))}
{results2.map((r) => (
<div>
{JSON.stringify(r)}
</div>
))}

</div>

// <CssVarsProvider modeStorageKey={LOCAL_STORAGE_KEY.THEME}>
// <QueryStatus/>
// </CssVarsProvider>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
io,
Socket,
} from "socket.io-client";

import {
MongoCollectionReactiveCursor,
type ServerError,
} from "./MongoCollectionReactiveCursor.js";


let sharedSocket: Socket | null = null;

/**
* Instantiate a shared socket connection to the server.
*
* @return The shared socket connection.
*/
const getSharedSocket = (): Socket => {
if (!sharedSocket) {
// You can pass a URL here if needed (e.g., from environment vars)
sharedSocket = io();
}

return sharedSocket;
};

/**
* Represents a MongoDB collection that can be queried over a socket connection.
*
* @class MongoCollection
*/
class MongoCollection {
private socket: Socket;

/**
* Creates an instance of MongoCollection.
*
* @param collectionName The name of the collection to interact with.
*/
constructor (collectionName: string) {
// eslint-disable-next-line no-warning-comments
// TODO: use the server URL from the environment / constructor args
this.socket = getSharedSocket();

this.socket.emit("collection::init", {
collectionName: collectionName,
}, (response: ServerError) => {
if ("error" in response && "collectionName" in response) {
if (collectionName === response.collectionName) {
console.error("Error initializing collection:", response.error);
}
}
});
}

// eslint-disable-next-line no-warning-comments
// TODO: add support for non-reactive cursors
/**
* Finds documents in the collection based on the provided query and options.
*
* @param query The query object to filter results.
* @param options The options for the query (e.g., sort, limit).
* @return An instance of MongoCollectionReactiveCursor for reactive querying.
*/
find (query: object, options: object) {
return new MongoCollectionReactiveCursor({
options: options,
query: query,
socket: this.socket,
});
}
}


export default MongoCollection;
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {Socket} from "socket.io-client";


interface CursorConstructorArgs {
socket: Socket;
query: object;
options: object;
}

/**
* Represents a cursor for querying a MongoDB collection over a socket connection.
*
* @class MongoCollectionCursor
*/
class MongoCollectionCursor {
socket: Socket;

findQuery: object;

findOptions: object;

/**
* Creates an instance of MongoCollectionCursor.
*
* @param args The constructor arguments.
* @param args.socket The socket connection to the server.
* @param args.query The query object to filter results.
* @param args.options The options for the query (e.g., sort, limit).
*/
constructor ({socket, query, options}: CursorConstructorArgs) {
this.socket = socket;

this.findQuery = query;
this.findOptions = options;
}

/**
* Adds a sort option to the query.
*
* @param sort The sort criteria.
* @return The current instance for method chaining.
*/
sort (sort: object): this {
this.findOptions = {
...this.findOptions,
sort: sort,
};

return this;
}

/**
* Sets a limit on the number of results returned.
*
* @param number The maximum number of results to return.
* @return The current instance for method chaining.
*/
limit (number: number): this {
this.findOptions = {
...this.findOptions,
limit: number,
};

return this;
}

/**
* Skips a specified number of results.
*
* @param offset The number of results to skip.
* @return The current instance for method chaining.
*/
skip (offset:number): this {
this.findOptions = {
...this.findOptions,
skip: offset,
};

return this;
}

/**
* Executes the query and returns the results as an array.
*
* @return A promise that resolves with the array of documents or rejects with an error.
*/
toArray () {
return new Promise((resolve, reject) => {
this.socket.emit("collection::find::toArray", {
query: this.findQuery,
options: this.findOptions,
}, (response: {error?: Error; data?: Document[]}) => {
if (response.error) {
reject(response.error);

return;
}

resolve(response.data);
});
});
}
}


export default MongoCollectionCursor;
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {Socket} from "socket.io-client";

import MongoCollectionCursor from "./MongoCollectionCursor.js";


interface ServerError {
collectionName?: string;
error: string;
queryId?: number;
}

interface Success<T> {
data: T;
}

type Response<T> = ServerError | Success<T>;

interface Listener {
onData: (data: Document[]) => void;
onError: (error: Error) => void;
}

interface ReactiveArrayCallback {
onData: (data: Document[]) => void;
onError: (error: Error) => void;
}

/**
* Represents a reactive cursor for querying a MongoDB-like collection over a socket connection.
* This class extends MongoCollectionCursor to provide real-time updates.
*
* @class MongoCollectionReactiveCursor
* @augments MongoCollectionCursor
*/
class MongoCollectionReactiveCursor extends MongoCollectionCursor {
// The listener for data and error events.
private listener: Listener | null = null;

// The unique identifier for the query.
private queryId: number | null = null;

/**
* Creates an instance of MongoCollectionReactiveCursor.
*
* @param props The constructor properties.
* @param props.socket The socket connection to the server.
* @param props.query The query object to filter results.
* @param props.options The options for the query (e.g., sort, limit).
*/
constructor (props: {socket: Socket; query: object; options: object}) {
super(props);

this.socket.on(
"collection::find::update",
(response: {error?: Error; data?: Document[]; queryId: number}) => {
if (this.queryId === response.queryId) {
if (response.error) {
return this.listener?.onError(response.error);
}

return this.listener?.onData(response.data ?? []);
}

return null;
}
);
}

/**
* Subscribe to the collection for real-time updates.
*
* @param callback
* @return The cleanup function.
*/
toReactiveArray (callback: ReactiveArrayCallback): () => void {
const unsubscribedInfo : {queryId: number | null} = {queryId: null};

this.socket.emit("collection::find::toReactiveArray", {
query: this.findQuery,
options: this.findOptions,
}, (response: Response<{queryId: number}>) => {
if ("error" in response) {
callback.onError(new Error(response.error));

return;
}

if ("undefined" !== typeof response.data.queryId) {
unsubscribedInfo.queryId = response.data.queryId;
this.queryId = response.data.queryId;
}

this.listener = callback;
});

return () => {
if (null === unsubscribedInfo.queryId) {
return;
}
this.socket.emit("collection::find::unsubscribe", {
queryId: unsubscribedInfo.queryId,
});

this.socket.off("collection::find::update");
};
}
}

export {MongoCollectionReactiveCursor};
export {type ServerError};
Loading